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) }
}
/* ─── 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 */
/* Desktop: top nav handles navigation; this dock stays hidden. */
/* 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-header">
<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>
<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
four dials for what that shift looks like right now: open permits, workers needed,
bench available, and coverage. The panel beside it adds combined bill demand for those
permits, deadline pressure on the open queue, the top roles in demand, and a shift
comparison strip. <strong>Click any shift</strong> in the comparison to filter every
dial and the bill rate to that shift's slice of the internal calendar — past, active,
or upcoming. Click the same shift again to return to the all-shifts total. This is the
<em>real world</em> the rest of the page is reacting to.
Live from the <strong>City of Chicago Open Data</strong> permit feed (Building Permits
≥ $250K), cross-referenced against our 500K-worker bench. The console on the left is
the punch clock — current time, today's active shift, and four dials watching
<strong>open permits</strong>, <strong>workers needed</strong>,
<strong>bench depth</strong>, and <strong>projected coverage</strong>. The panel on
the right reads the same feed financially: combined bill demand if every permit fills,
deadline pressure across <em>overdue / urgent / soon / scheduled</em>, the four roles
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>
<div id="live-market-hero" style="display:grid;grid-template-columns:240px 1fr;gap:20px;align-items:start">
<div style="grid-column:1/-1">
@ -2355,8 +2499,12 @@ function loadLiveContracts(){
var p=c.permit||{}, prop=c.proposed||{}, tl=c.timeline||{};
var urg=tl.urgency||'scheduled';
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;
// 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
var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;margin-bottom:8px;gap:12px';
var left=document.createElement('div');
@ -2416,6 +2564,48 @@ function loadLiveContracts(){
}
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"
// as a horizontal bar of cumulative percentages. Drill down that
// 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';
fpLabel.appendChild(fpTitle);fpLabel.appendChild(fpBase);
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');
fpBar.className='fp-bar';
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){
var prev=idx===0?0:fp.curve[idx-1].cumulative_pct;
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.textContent='Cumulative chance the role gets fully staffed by that point, given pool depth, urgency, and past fill patterns.';
fpRow.appendChild(fpNote);
card.appendChild(fpRow);
detailsInner.appendChild(fpRow);
}
// 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';
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));
card.appendChild(ecRow);
detailsInner.appendChild(ecRow);
}
// Project Index — portfolio of public-data signals for this permit's
@ -2572,22 +2780,13 @@ function loadLiveContracts(){
ebBody.appendChild(errDiv);
});
});
card.appendChild(eb);
detailsInner.appendChild(eb);
}
// Description
if(p.description){
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);
}
// 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);
desc.textContent=p.description;detailsInner.appendChild(desc);
}
// Candidates
var cands=prop.candidates||[];
@ -2615,13 +2814,52 @@ function loadLiveContracts(){
sub2.textContent=subText;
info.appendChild(nm);info.appendChild(sub2);
row.appendChild(av);row.appendChild(info);
card.appendChild(row);
detailsInner.appendChild(row);
});
if(cands.length>3){
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';
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);
});
}).catch(function(e){