Post-PR-#11 polish: demo UI, staffer console, face pool, icons, contractor profile (24 commits) #12

Merged
profit merged 44 commits from demo/post-pr11-polish-2026-04-28 into main 2026-05-03 05:16:17 +00:00
Showing only changes of commit f4dc1b29e3 - Show all commits

View File

@ -202,6 +202,147 @@ body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;backgroun
50% { opacity:1; transform:scale(1.1) } 50% { opacity:1; transform:scale(1.1) }
} }
/* ─── Fill-probability bar — viewport-triggered paint + shimmer ─────
The bar paints in left-to-right via a clip-path animation so the
green → gold → orange → red gradient reads as a *timeline growing*
instead of a static heatmap with "danger zone" at the right edge.
Triggering: the .fp-bar starts in a fully-clipped state and only
paints when JS adds .lit via IntersectionObserver — fires when the
bar enters viewport, so a fast scroller sees every bar animate as
it scrolls past, instead of missing the show on first paint.
Timing: 350ms entry delay (lets the eye lock onto the bar before
motion starts) + 2800ms paint (slow enough to actually watch the
timeline build). Easing matches the card-in stagger (cubic-bezier
.2 .7 .2 1) so the page reads as one consistent motion language.
Shimmer: 30%-wide highlight sweeps across every ~3.4s on a 3400ms
delay (350 entry + 2800 paint + 250 dwell) so the two motions
never compete — the bar finishes drawing, *then* it pulses. */
@keyframes fp-paint {
from { clip-path: inset(0 100% 0 0); }
to { clip-path: inset(0 0 0 0); }
}
@keyframes fp-shimmer {
0% { transform: translateX(-100%); opacity: 0 }
10% { opacity: 1 }
60% { transform: translateX(260%); opacity: 1 }
61%, 100% { transform: translateX(260%); opacity: 0 }
}
.fp-bar {
position: relative;
/* Hidden until .lit is added — IntersectionObserver fires the paint
animation by toggling the class, which is the only thing that
unmasks the clip-path. */
clip-path: inset(0 100% 0 0);
}
.fp-bar.lit {
animation: fp-paint 2800ms cubic-bezier(.2,.7,.2,1) 350ms both;
}
.fp-bar::after {
content: "";
position: absolute;
top: 0; left: 0;
width: 30%;
height: 100%;
background: linear-gradient(90deg,
transparent 0%,
rgba(255,255,255,0.18) 50%,
transparent 100%);
pointer-events: none;
}
.fp-bar.lit::after {
animation: fp-shimmer 3.4s ease-in-out 3400ms infinite;
}
@media (prefers-reduced-motion: reduce) {
/* Skip the clip-path hide too — otherwise reduced-motion users
would see permanently invisible bars. The .lit class still gets
added by JS but the animation is `none`, so bar just appears. */
.fp-bar { clip-path: none }
.fp-bar.lit { animation: none }
.fp-bar.lit::after { animation: none; opacity: 0 }
}
/* ─── Staffer's Console: compact contract card with click-to-expand ─
Each card stays compact by default so a coordinator scanning 20+
contracts sees ~5 cards per viewport instead of ~1.5. The summary
strip below the pills shows revenue / margin / fill-by-1wk / top
candidate so scanners get the punchline without expanding. The
whole card surface is clickable; clicks inside the expanded details
don't bubble to the toggle (so contractor links / SMS copy still
work). When a card expands, its Project Index auto-opens too —
solves the "users aren't finding the build signals" gap without
firing 20× OSHA scrapes on page load. */
.contract-card {
cursor: pointer;
transition: transform 180ms ease, box-shadow 180ms ease;
}
.contract-card:hover {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(0,0,0,0.32), 0 0 0 1px #30363d;
}
.contract-card.expanded { cursor: default }
.contract-card.expanded:hover { transform: none }
.contract-card .card-strip {
background: #0d1117;
border: 1px solid #171d27;
border-radius: 8px;
padding: 8px 12px;
margin-bottom: 10px;
display: flex;
flex-wrap: wrap;
gap: 14px;
font-size: 11px;
color: #8b949e;
align-items: baseline;
}
.contract-card .card-strip strong {
font-weight: 600;
font-variant-numeric: tabular-nums;
margin-right: 3px;
}
/* Modern smooth-expand: grid-template-rows transitions cleanly to
the actual content height. Safari 17.4+ / Chrome 117+ / FF 117+. */
.contract-card .card-details {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 380ms cubic-bezier(.2,.7,.2,1);
}
.contract-card.expanded .card-details { grid-template-rows: 1fr }
.contract-card .card-details > .card-details-inner {
overflow: hidden;
min-height: 0;
}
.contract-card.expanded .card-details > .card-details-inner {
padding-top: 4px;
}
.contract-card .card-toggle {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 0 2px;
font-size: 10px;
color: #545d68;
letter-spacing: 1.5px;
text-transform: uppercase;
user-select: none;
}
.contract-card .card-toggle .chevron {
display: inline-block;
transition: transform 320ms cubic-bezier(.2,.7,.2,1);
font-size: 12px;
line-height: 1;
}
.contract-card.expanded .card-toggle .chevron { transform: rotate(180deg) }
@media (prefers-reduced-motion: reduce) {
.contract-card { transition: none }
.contract-card:hover { transform: none }
.contract-card .card-details { transition: none }
.contract-card .card-toggle .chevron { transition: none }
}
/* Bottom section-jump nav — mobile only */ /* Bottom section-jump nav — mobile only */
/* Desktop: top nav handles navigation; this dock stays hidden. */ /* Desktop: top nav handles navigation; this dock stays hidden. */
/* Mobile: top nav collapses (.bar nav:display:none below); this fixed dock */ /* Mobile: top nav collapses (.bar nav:display:none below); this fixed dock */
@ -503,17 +644,20 @@ body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;backgroun
<div class="section" id="live-market-section"> <div class="section" id="live-market-section">
<div class="section-header"> <div class="section-header">
<span class="section-title" style="color:#e6edf3;font-size:13px">① Live Market — Chicago right now</span> <span class="section-title" style="color:#e6edf3;font-size:13px">① Live Market — Chicago right now</span>
<span class="section-meta" id="live-market-meta">Public permit data · per-shift schedule on the internal calendar · click any shift to filter</span> <span class="section-meta" id="live-market-meta">City of Chicago Open Data · cross-referenced against the 500K-worker bench · click any shift to slice the dials</span>
</div> </div>
<p style="color:#8b949e;font-size:11px;margin:0 0 12px;line-height:1.5;max-width:780px"> <p style="color:#8b949e;font-size:11px;margin:0 0 12px;line-height:1.5;max-width:780px">
The console on the left is the live punch clock — current time, the active shift, and Live from the <strong>City of Chicago Open Data</strong> permit feed (Building Permits
four dials for what that shift looks like right now: open permits, workers needed, ≥ $250K), cross-referenced against our 500K-worker bench. The console on the left is
bench available, and coverage. The panel beside it adds combined bill demand for those the punch clock — current time, today's active shift, and four dials watching
permits, deadline pressure on the open queue, the top roles in demand, and a shift <strong>open permits</strong>, <strong>workers needed</strong>,
comparison strip. <strong>Click any shift</strong> in the comparison to filter every <strong>bench depth</strong>, and <strong>projected coverage</strong>. The panel on
dial and the bill rate to that shift's slice of the internal calendar — past, active, the right reads the same feed financially: combined bill demand if every permit fills,
or upcoming. Click the same shift again to return to the all-shifts total. This is the deadline pressure across <em>overdue / urgent / soon / scheduled</em>, the four roles
<em>real world</em> the rest of the page is reacting to. being asked for most, and a shift-mix bar. <strong>Click any shift bar</strong> to
re-slice the dials and the dollar counter to that shift's calendar slice; click again
to clear. The same permit feed drives the staffing forecast, the staffer's console,
and worker matches further down — this row is its heartbeat.
</p> </p>
<div id="live-market-hero" style="display:grid;grid-template-columns:240px 1fr;gap:20px;align-items:start"> <div id="live-market-hero" style="display:grid;grid-template-columns:240px 1fr;gap:20px;align-items:start">
<div style="grid-column:1/-1"> <div style="grid-column:1/-1">
@ -2355,8 +2499,12 @@ function loadLiveContracts(){
var p=c.permit||{}, prop=c.proposed||{}, tl=c.timeline||{}; var p=c.permit||{}, prop=c.proposed||{}, tl=c.timeline||{};
var urg=tl.urgency||'scheduled'; var urg=tl.urgency||'scheduled';
var borderColor={overdue:'#f85149',urgent:'#d29922',soon:'#388bfd',scheduled:'#2ea043'}[urg]||'#388bfd'; var borderColor={overdue:'#f85149',urgent:'#d29922',soon:'#388bfd',scheduled:'#2ea043'}[urg]||'#388bfd';
var card=document.createElement('div');card.className='insight info'; var card=document.createElement('div');card.className='insight info contract-card';
card.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:16px;margin-bottom:10px;border-left:3px solid '+borderColor; card.style.cssText='background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:16px;margin-bottom:10px;border-left:3px solid '+borderColor;
// Foldable details container — fpRow / ecRow / Project Index /
// description / candidate rows all live here. Hidden until the
// card gets .expanded; CSS animates the grid-template-rows.
var detailsInner=document.createElement('div');detailsInner.className='card-details-inner';
// Header — permit // Header — permit
var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;margin-bottom:8px;gap:12px'; var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;margin-bottom:8px;gap:12px';
var left=document.createElement('div'); var left=document.createElement('div');
@ -2416,6 +2564,48 @@ function loadLiveContracts(){
} }
if(pillRow.childNodes.length) card.appendChild(pillRow); if(pillRow.childNodes.length) card.appendChild(pillRow);
// Compact summary strip — at-a-glance numbers so scanners get
// the headline without expanding. Mirrors the colors used in
// the full economics grid below: revenue green, margin tiered.
var strip=document.createElement('div');strip.className='card-strip';
function stripCell(label,value,color){
var s=document.createElement('span');
var b=document.createElement('strong');b.style.color=color||'#e6edf3';b.textContent=value;
s.appendChild(b);s.appendChild(document.createTextNode(label));
return s;
}
if(c.economics){
strip.appendChild(stripCell(' rev','$'+Math.round(c.economics.gross_revenue/1000)+'K','#3fb950'));
var stripMarginColor=c.economics.margin_pct>=25?'#3fb950':c.economics.margin_pct>=10?'#d29922':'#f85149';
strip.appendChild(stripCell(' margin',c.economics.margin_pct+'%',stripMarginColor));
}
if(c.fill_probability&&c.fill_probability.curve&&c.fill_probability.curve.length){
var bucket7=c.fill_probability.curve.find(function(pt){return pt.day===7});
var fillBucket=bucket7||c.fill_probability.curve[0];
var spanLabel=bucket7?' fill by 1wk':' fill by day '+fillBucket.day;
strip.appendChild(stripCell(spanLabel,fillBucket.cumulative_pct+'%','#58a6ff'));
}
var topCand=(prop.candidates||[])[0];
if(topCand&&topCand.name){
// Abbreviate "Maria Sanchez" → "Maria S." so the strip stays one line.
var nameParts=topCand.name.trim().split(/\s+/);
var topShort=nameParts.length>1?nameParts[0]+' '+(nameParts[nameParts.length-1][0]||'')+'.':nameParts[0];
strip.appendChild(stripCell(' top match',topShort,'#e6edf3'));
}
if(strip.childNodes.length) card.appendChild(strip);
// Pattern (meta-index) chip — kept visible on the compact card
// because it's a glance-worthy "we have playbooks for this kind
// of contract" signal that doesn't take much vertical space.
if(c.discovered_pattern){
var pat=document.createElement('div');pat.style.cssText='background:#0d2818;border:1px solid #2ea04360;border-radius:6px;padding:8px 12px;margin-bottom:10px;font-size:11px;color:#86efac;line-height:1.5';
var plabel=document.createElement('span');plabel.style.cssText='color:#3fb950;font-weight:600;margin-right:6px';
plabel.textContent='MEMORY ('+c.pattern_matched+' playbooks):';
pat.appendChild(plabel);
pat.appendChild(document.createTextNode(' '+c.discovered_pattern));
card.appendChild(pat);
}
// Fill-probability curve — shows "likelihood of filling by day N" // Fill-probability curve — shows "likelihood of filling by day N"
// as a horizontal bar of cumulative percentages. Drill down that // as a horizontal bar of cumulative percentages. Drill down that
// J asked for: "percentage likelihood of filling them on a certain time." // J asked for: "percentage likelihood of filling them on a certain time."
@ -2429,9 +2619,27 @@ function loadLiveContracts(){
var fpBase=document.createElement('span');fpBase.textContent='base '+fp.base_pct+'% · pool × urgency'; var fpBase=document.createElement('span');fpBase.textContent='base '+fp.base_pct+'% · pool × urgency';
fpLabel.appendChild(fpTitle);fpLabel.appendChild(fpBase); fpLabel.appendChild(fpTitle);fpLabel.appendChild(fpBase);
fpRow.appendChild(fpLabel); fpRow.appendChild(fpLabel);
// Horizontal stacked bar — each bucket as a segment // Horizontal stacked bar — each bucket as a segment.
// .fp-bar starts hidden (clip-path); IntersectionObserver adds
// .lit when the bar enters viewport, which fires the paint +
// shimmer animations defined in the <style> block above.
var fpBar=document.createElement('div'); var fpBar=document.createElement('div');
fpBar.className='fp-bar';
fpBar.style.cssText='display:flex;height:8px;border-radius:4px;overflow:hidden;background:#161b22;margin-bottom:6px'; fpBar.style.cssText='display:flex;height:8px;border-radius:4px;overflow:hidden;background:#161b22;margin-bottom:6px';
// Lazy-init a single shared observer — the same instance can
// watch every .fp-bar on the page. unobserve() after firing so
// the bar doesn't re-paint if the user scrolls back past it.
if(!window._lhFpObserver){
window._lhFpObserver=new IntersectionObserver(function(entries){
entries.forEach(function(e){
if(e.isIntersecting){
e.target.classList.add('lit');
window._lhFpObserver.unobserve(e.target);
}
});
},{ threshold: 0.2 });
}
window._lhFpObserver.observe(fpBar);
fp.curve.forEach(function(pt,idx){ fp.curve.forEach(function(pt,idx){
var prev=idx===0?0:fp.curve[idx-1].cumulative_pct; var prev=idx===0?0:fp.curve[idx-1].cumulative_pct;
var delta=pt.cumulative_pct-prev; var delta=pt.cumulative_pct-prev;
@ -2471,7 +2679,7 @@ function loadLiveContracts(){
fpNote.style.cssText='font-size:9px;color:#545d68;margin-top:6px;line-height:1.4'; fpNote.style.cssText='font-size:9px;color:#545d68;margin-top:6px;line-height:1.4';
fpNote.textContent='Cumulative chance the role gets fully staffed by that point, given pool depth, urgency, and past fill patterns.'; fpNote.textContent='Cumulative chance the role gets fully staffed by that point, given pool depth, urgency, and past fill patterns.';
fpRow.appendChild(fpNote); fpRow.appendChild(fpNote);
card.appendChild(fpRow); detailsInner.appendChild(fpRow);
} }
// Economics panel — "as though the contracts were accepted and filled" // Economics panel — "as though the contracts were accepted and filled"
@ -2497,7 +2705,7 @@ function loadLiveContracts(){
var overColor=ec.over_bill_count>0?'#f85149':'#8b949e'; var overColor=ec.over_bill_count>0?'#f85149':'#8b949e';
ecRow.appendChild(ecCell('Over-Bill Pool',ec.over_bill_count+'/'+(prop.candidates||[]).length, ecRow.appendChild(ecCell('Over-Bill Pool',ec.over_bill_count+'/'+(prop.candidates||[]).length,
ec.over_bill_count>0?'$'+ec.over_bill_pool_margin_at_risk.toLocaleString()+' at risk':'none flagged',overColor)); ec.over_bill_count>0?'$'+ec.over_bill_pool_margin_at_risk.toLocaleString()+' at risk':'none flagged',overColor));
card.appendChild(ecRow); detailsInner.appendChild(ecRow);
} }
// Project Index — portfolio of public-data signals for this permit's // Project Index — portfolio of public-data signals for this permit's
@ -2572,22 +2780,13 @@ function loadLiveContracts(){
ebBody.appendChild(errDiv); ebBody.appendChild(errDiv);
}); });
}); });
card.appendChild(eb); detailsInner.appendChild(eb);
} }
// Description // Description
if(p.description){ if(p.description){
var desc=document.createElement('div');desc.style.cssText='color:#94a3b8;font-size:11px;margin-bottom:10px;line-height:1.5'; var desc=document.createElement('div');desc.style.cssText='color:#94a3b8;font-size:11px;margin-bottom:10px;line-height:1.5';
desc.textContent=p.description;card.appendChild(desc); desc.textContent=p.description;detailsInner.appendChild(desc);
}
// Pattern (meta-index) chip
if(c.discovered_pattern){
var pat=document.createElement('div');pat.style.cssText='background:#0d2818;border:1px solid #2ea04360;border-radius:6px;padding:8px 12px;margin-bottom:10px;font-size:11px;color:#86efac;line-height:1.5';
var plabel=document.createElement('span');plabel.style.cssText='color:#3fb950;font-weight:600;margin-right:6px';
plabel.textContent='MEMORY ('+c.pattern_matched+' playbooks):';
pat.appendChild(plabel);
pat.appendChild(document.createTextNode(' '+c.discovered_pattern));
card.appendChild(pat);
} }
// Candidates // Candidates
var cands=prop.candidates||[]; var cands=prop.candidates||[];
@ -2615,13 +2814,52 @@ function loadLiveContracts(){
sub2.textContent=subText; sub2.textContent=subText;
info.appendChild(nm);info.appendChild(sub2); info.appendChild(nm);info.appendChild(sub2);
row.appendChild(av);row.appendChild(info); row.appendChild(av);row.appendChild(info);
card.appendChild(row); detailsInner.appendChild(row);
}); });
if(cands.length>3){ if(cands.length>3){
var more=document.createElement('div');more.style.cssText='font-size:10px;color:#58a6ff;padding:4px 10px;margin-top:2px'; var more=document.createElement('div');more.style.cssText='font-size:10px;color:#58a6ff;padding:4px 10px;margin-top:2px';
more.textContent='+ '+(cands.length-3)+' more candidates available'; more.textContent='+ '+(cands.length-3)+' more candidates available';
card.appendChild(more); detailsInner.appendChild(more);
} }
// Wrap the foldable section + add it to the card. The CSS
// .card-details rule animates grid-template-rows: 0fr → 1fr
// when the parent gets .expanded, so this whole block grows
// smoothly to its natural height when the user clicks.
var details=document.createElement('div');details.className='card-details';
details.appendChild(detailsInner);
card.appendChild(details);
// Toggle affordance — small caret + label at the bottom. CSS
// rotates the chevron 180° when expanded; the label flips text
// via the click handler below.
var toggleRow=document.createElement('div');toggleRow.className='card-toggle';
var chevron=document.createElement('span');chevron.className='chevron';chevron.textContent='▾';
var toggleLabel=document.createElement('span');
toggleLabel.textContent='click for full curve, economics, candidates, project index';
toggleRow.appendChild(chevron);toggleRow.appendChild(toggleLabel);
card.appendChild(toggleRow);
// Click handler — expand/collapse + auto-open Project Index when
// expanding. Skips the toggle when the click landed inside the
// already-expanded details (so contractor links / SMS copy /
// <details> toggles inside still work) and when the user is
// selecting text (so a triple-click drag doesn't snap it shut).
card.addEventListener('click',function(e){
if(e.target.closest('.card-details')) return;
if(window.getSelection&&window.getSelection().toString().length>0) return;
var nowExpanded=!card.classList.contains('expanded');
card.classList.toggle('expanded');
toggleLabel.textContent=nowExpanded
? 'click to collapse'
: 'click for full curve, economics, candidates, project index';
// Auto-open the Project Index ONLY on user-driven expand. This
// avoids firing 20+ OSHA scrapes on page load — each open
// triggers /intelligence/permit_entities, which live-scrapes
// OSHA at ~1-2s per contractor.
if(nowExpanded&&typeof eb!=='undefined'&&eb&&!eb.open) eb.open=true;
});
el.appendChild(card); el.appendChild(card);
}); });
}).catch(function(e){ }).catch(function(e){