diff --git a/mcp-server/search.html b/mcp-server/search.html index ff9add7..d1f1eb9 100644 --- a/mcp-server/search.html +++ b/mcp-server/search.html @@ -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
① Live Market — Chicago right now - +

- 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. Click any shift 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 - real world the rest of the page is reacting to. + Live from the City of Chicago Open Data 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 + open permits, workers needed, + bench depth, and projected coverage. The panel on + the right reads the same feed financially: combined bill demand if every permit fills, + deadline pressure across overdue / urgent / soon / scheduled, the four roles + being asked for most, and a shift-mix bar. Click any shift bar 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.

@@ -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