/* ════════════════════════════════════════════════════════════════════════════
   Shared panel ("pane") styles — the single source of truth for how every bento
   looks. Loaded by BOTH the hero (index.html) and the flat gallery (panels.html).
   Owns pane CONTENT/brand styling only (card chrome + each pane's layout + mobile
   rules + the Spotify waveform + header brand logos). NOT page layout: the hero
   positions panels absolutely on its skewed plane; the gallery overrides `.panel`
   to flow on a flat grid. Pairs with the pane modules under panels/ (one file per
   pane; assembled by panels/index.js into window.Panels). This stylesheet is still
   ONE shared file, sectioned per pane below — keep edits here, both pages pick them
   up. Build guide: knowledge/building-a-panel.md.
   ════════════════════════════════════════════════════════════════════════════ */

.panel {
  pointer-events:auto; position:absolute;
  background:#fff; border:1px solid rgba(0,0,0,.07); border-radius:10px;
  box-shadow:0 18px 30px -14px rgba(0,0,0,.22), 0 2px 5px rgba(0,0,0,.07);
  padding:12px; overflow:hidden; cursor:pointer; transition:box-shadow .2s ease;
  --accent: #6b6b70;   /* neutral default; each panel-class overrides with its brand color */
}
/* top accent hairline — a 3px stripe pinned to the top of every bento,
   painted with the panel's --accent. */
.panel::before {
  content:""; position:absolute; left:0; right:0; top:0; height:3px;
  background:var(--accent); opacity:.9;
}
.panel:hover { box-shadow:0 30px 46px -16px rgba(0,0,0,.30), 0 3px 8px rgba(0,0,0,.10); }
/* Whole-panel click target → the pane's profile (GitHub/X/Strava/Spotify/LinkedIn).
   An invisible anchor stretched over the whole card; the card already shows
   cursor:pointer. Sits just above the card surface (z-index:1) but BELOW any
   genuinely-interactive child (per-post links, the greetings button) which opt
   back on top with `position:relative; z-index:2`. So a click on empty card area
   goes to the profile, while a click on a specific link still does its own thing. */
.panel .panel-link { position:absolute; inset:0; z-index:1; border-radius:inherit;
  -webkit-tap-highlight-color:transparent; text-decoration:none; }
.panel .pn { position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  font:800 56px/1 system-ui,Arial,sans-serif; color:rgba(0,0,0,.20); pointer-events:none; }
.panel .sk-dot  { display:block; width:20px; height:20px; border-radius:6px; background:var(--accent); opacity:.85; }
/* The pane-header brand logos that replaced the old colored status dots. The `*-dot`
   span is now the sized, brand-colored box (color → currentColor); this svg fills it. */
.panel .pane-logo { display:block; width:100%; height:100%; }
.panel .sk-line { display:block; height:8px; border-radius:4px; background:rgba(0,0,0,.07); margin-top:9px; }
.panel .l1 { width:72%; } .panel .l2 { width:50%; }
/* decorative mock panel — placeholder kept so the bento still shows its top
   accent stripe (mockHTML). #2/#5/#6 are live panels and #8 is now the ProdBreak
   preview (see its section at the foot of this file), so `.panel.m3` is the only
   accent the mock builder still needs. */
.panel.m3 { --accent:#6e56cf; }   /* purple — icons */

/* frame #1 hosts the FULL GitHub panel — authentic GitHub-green levels, inheriting the
   plane's skew. Layout rule (Franco): the contribution GRID always occupies 4/9 of the
   frame's height; it spans the content width (within the panel padding) and shows as
   many recent weeks as fit, cut cleanly on a week boundary. Keeps the month labels on
   top and the Less→More legend below (the full component). row/column tracks are
   justified with space-between so the grid fills the content box edge to edge.
   JetBrains Mono for data, Chakra Petch for the name. */
.panel.gh { --gh:#2ea043; --accent:var(--gh); --muted:#6b6b70; --hair:rgba(0,0,0,.10);
  --l0:#ebedf0; --l1:#9be9a8; --l2:#40c463; --l3:#30a14e; --l4:#216e39; --sq:11px; --cols:20;
  padding:14px 16px; font-family:"JetBrains Mono",ui-monospace,monospace; color:var(--ink);
  display:flex; flex-direction:column;
  container-type:size; container-name:gh-pane; }   /* size container so the profile strip can self-prune when the box is too small (see the @container rules below the strip styles); the hero sizes panes per-breakpoint, so this reacts to the pane, not the viewport */
.panel.gh .gh-info { flex:1 1 auto; min-height:0; overflow:hidden; }
.panel.gh .gh-head { display:flex; align-items:center; gap:9px; margin-bottom:12px; }
.panel.gh .gh-dot { width:15px; height:15px; flex:0 0 auto; color:#181717; }   /* GitHub octocat — canonical black */
.panel.gh .gh-lbl { font-family:"Chakra Petch",sans-serif; font-weight:600;
  text-transform:uppercase; letter-spacing:.14em; font-size:11px; color:var(--muted); }
.panel.gh .gh-meta { margin-left:auto; font-size:11px; color:var(--muted); }
.panel.gh .gh-top { display:flex; align-items:center; gap:11px; }
.panel.gh .gh-avatar { width:42px; height:42px; border-radius:10px; background:#e3e3e3;
  flex:0 0 42px; aspect-ratio:1/1; object-fit:cover; object-position:center;
  align-self:flex-start; border:1px solid var(--hair); }
.panel.gh .gh-id { min-width:0; line-height:1.2; }
.panel.gh .gh-name { font-family:"Chakra Petch",sans-serif; font-weight:700; font-size:15px; }
.panel.gh .gh-handle { font-size:11px; color:var(--muted); display:inline-block;
  position:relative; z-index:2; }   /* keep this a real, focusable link above the whole-panel overlay */
.panel.gh .gh-bio { font-size:12px; color:#2a2a2d; line-height:1.4; margin:9px 0 0; max-width:48ch;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.panel.gh .gh-stats { display:flex; gap:15px; margin:9px 0 0; font-size:11px; color:var(--muted); }
.panel.gh .gh-stats b { color:var(--ink); font-weight:700; }

/* ── Profile strip self-pruning ────────────────────────────────────────────────
   The contribution grid is pinned at 4/9 of the frame height + ~38px of month/
   legend chrome, so the room left ABOVE it for the profile strip is small and
   shrinks fast as the pane shortens. If a strip element only PARTLY fits it gets
   sliced to a sliver under the header — those slivers are the "tiny lines" that
   showed above the chart on mobile. So instead of clipping, we DROP each element
   the moment there isn't full room for it (Franco's rule: "if it can't fit, it
   just shouldn't be there"), tallest-cost element first: bio+stats → avatar →
   name/handle, until only the header + calendar remain on the smallest panes.

   Thresholds are on the container's CONTENT box (container queries ignore the
   14px padding) and were derived from the room equation gh-info ≈ (5/9)·frameH−66:
     • > 335px  full strip (avatar + name + handle + bio + stats) — desktop ≥ ~1440w
     • ≤ 335px  drop bio + stats        (narrow desktop / tablet)
     • ≤ 245px  also drop the avatar    (portrait tablet)
     • ≤ 215px  also drop name + handle (phones) → header + calendar only
   Container-based, not @media: the hero sizes panes per-breakpoint independent of
   the viewport, so this reacts to the real pane box at every width. The width
   guard collapses to the minimal header+calendar on any freakishly narrow pane.
   NB: multi-condition preludes MUST use the `or` keyword — a comma list is invalid
   container-query syntax and silently drops the whole rule. */
@container gh-pane (max-height: 335px) {
  .panel.gh .gh-bio, .panel.gh .gh-stats { display:none; }
}
@container gh-pane (max-height: 245px) {
  .panel.gh .gh-avatar { display:none; }
}
@container gh-pane (max-height: 215px) or (max-width: 200px) {
  .panel.gh .gh-top { display:none; }   /* drops name + handle (and the avatar it wraps) → header + calendar only */
}
/* calendar block, pinned to the bottom of the frame */
.panel.gh .gh-cal { flex:0 0 auto; }
.panel.gh .gh-months { display:grid; grid-template-columns:repeat(var(--cols), var(--sq));
  justify-content:space-between; height:14px; margin-bottom:5px; font-size:11px; color:var(--muted); }
.panel.gh .gh-months span { grid-row:1; white-space:nowrap; }
/* the grid: height = 4/9 of the frame (set in JS); 7 weekday rows and the week columns
   are spread edge-to-edge across the content box with space-between. */
.panel.gh .gh-grid { display:grid;
  grid-template-columns:repeat(var(--cols), var(--sq)); grid-template-rows:repeat(7, var(--sq));
  grid-auto-flow:column; justify-content:space-between; align-content:space-between; }
.panel.gh .gh-grid i { border-radius:2px; }
.panel.gh [data-l]{ background:var(--l0); }
.panel.gh [data-l="1"]{ background:var(--l1); }
.panel.gh [data-l="2"]{ background:var(--l2); }
.panel.gh [data-l="3"]{ background:var(--l3); }
.panel.gh [data-l="4"]{ background:var(--l4); }
.panel.gh .gh-grid i.pad{ background:transparent; }
.panel.gh .gh-legend { display:flex; align-items:center; justify-content:flex-end; gap:3px;
  margin-top:8px; font-size:9px; color:var(--muted); }
.panel.gh .gh-legend i { width:10px; height:10px; border-radius:2px; display:inline-block; }

/* frame #4 = CUSTOM Strava "latest activity" card. Reads strava-latest.json (written
   server-side by the GitHub Action) so the latest activity is public with no per-visit
   login; falls back to a placeholder before the data exists. The route is an inline SVG
   decoded from the activity's polyline (vector -> crisp, inherits the plane's skew). */
.panel.strava { --strava:#fc4c02; --accent:var(--strava); --muted:#6b6b70; --hair:rgba(0,0,0,.10);
  padding:14px 16px; font-family:"JetBrains Mono",ui-monospace,monospace; color:var(--ink);
  display:flex; flex-direction:column;
  container-type:inline-size; container-name:sv-pane; }   /* so the CTA can drop " on Strava" on narrow panes */
.panel.strava .sv-head { display:flex; align-items:center; gap:9px; margin-bottom:8px; flex:0 0 auto; }
.panel.strava .sv-dot { width:15px; height:15px; flex:0 0 auto; color:var(--strava); }
.panel.strava .sv-lbl { font-family:"Chakra Petch",sans-serif; font-weight:600;
  text-transform:uppercase; letter-spacing:.14em; font-size:11px; color:var(--muted); }
.panel.strava .sv-meta { margin-left:auto; font-size:11px; color:var(--muted); }
.panel.strava .sv-name { font-family:"Chakra Petch",sans-serif; font-weight:700;
  font-size:16px; line-height:1.15; flex:0 0 auto; display:-webkit-box; -webkit-line-clamp:2;
  -webkit-box-orient:vertical; overflow:hidden; }
.panel.strava .sv-sub { font-size:11px; color:var(--muted); margin-top:3px; }
/* The map is the flex-fill region: it absorbs slack so the stats + CTA below it
   sit at the bottom of the card. min-height:0 lets it shrink on short frames
   (the hero packs small) so nothing clips and the CTA stays visible at the bottom. */
.panel.strava .sv-map { flex:1 1 auto; min-height:0; width:100%; margin:11px 0; display:block; }
/* basemap mode: the div frames the map (rounded + hairline). The actual Leaflet
   container is an inner element rendered at SS× size and scaled back down here —
   supersampling that keeps the raster basemap crisp when the panel is displayed
   larger (esp. on the hero's skewed plane). No CSS filter on the tiles: a filter
   forces a compositing layer that the plane's transform then resamples = soft. */
.panel.strava .sv-map.has-basemap { position:relative; overflow:hidden;
  border-radius:8px; border:1px solid var(--hair); background:#e8e9ef; }
.panel.strava .sv-map-ss { position:absolute; top:0; left:0;
  width:calc(100% * var(--ss, 2)); height:calc(100% * var(--ss, 2));
  transform:scale(calc(1 / var(--ss, 2))); transform-origin:top left; }
/* cool lavender-grey pre-load bg matching Mapbox Streets' land, so no warm/grey
   flash before tiles paint. */
.panel.strava .leaflet-container { background:#e8e9ef; font:inherit; }
/* Map credit (Mapbox + OSM, required by Mapbox's ToS). Kept tiny + faint so it
   reads like the reference's subtle "© OpenStreetMap" and doesn't fight the
   minimal card. It's rendered into the SS× inner element, so the font-size is
   pre-divided by SS to land at ~7px on screen after the scale-down. */
.panel.strava .leaflet-control-attribution {
  background:rgba(255,255,255,.6); color:#9a9aa2;
  font-size:calc(7px * var(--ss, 2)); line-height:1.3;
  padding:0 calc(3px * var(--ss, 2)); }
.panel.strava .leaflet-control-attribution a { color:#7a7a82; text-decoration:none; }
/* bare-route fallback (no basemap) */
.panel.strava .sv-route { width:100%; height:100%; display:block; }
.panel.strava .sv-route path { fill:none; stroke:var(--strava); stroke-width:2.5;
  stroke-linejoin:round; stroke-linecap:round; vector-effect:non-scaling-stroke; }
/* space-between hugs both padding edges so the three figures (KM / TIME / PACE)
   spread edge-to-edge of the content box without crowding the card border. The
   label carries the unit, so the number has no inline unit to wrap. */
.panel.strava .sv-stats { display:flex; justify-content:space-between; gap:8px; flex:0 0 auto; }
.panel.strava .sv-stat .n { font-family:"Chakra Petch",sans-serif; font-weight:700; font-size:18px; line-height:1; }
.panel.strava .sv-stat .k { font-size:9px; text-transform:uppercase; letter-spacing:.12em; color:var(--muted); margin-top:4px; }
/* CTA pinned to the very bottom of the card. The stats sit directly above it
   (fixed gap); everything above the stats is absorbed by the flex-fill map, so
   the CTA always lands on the bottom edge regardless of frame height.
   z-index:2 keeps it a real, focusable link above the whole-panel overlay.
   nowrap + the container query below keep it to ONE line — on a narrow pane the
   " on Strava" suffix is dropped so it reads just "Follow me →". */
.panel.strava .sv-cta { margin-top:14px; flex:0 0 auto; align-self:flex-start;
  position:relative; z-index:2; white-space:nowrap; text-decoration:none;
  display:inline-flex; align-items:center; gap:6px; font-size:11px; font-weight:700;
  color:var(--strava); text-transform:uppercase; letter-spacing:.08em; }
/* underline the label only (not the arrow) — matches the Spotify CTA */
.panel.strava .sv-cta .sv-cta-text { text-decoration:underline; text-underline-offset:3px; }
.panel.strava .sv-cta .sv-cta-arrow { transition:transform .2s ease; }
.panel.strava:hover .sv-cta .sv-cta-arrow { transform:translateX(3px); }
/* Narrow pane → "Follow me" (drop " on Strava") so the CTA never wraps. ~210px
   is roughly where "Follow me on Strava" stops fitting on one line at this size. */
@container sv-pane (max-width: 210px){ .panel.strava .sv-cta-suffix { display:none; } }
/* mobile: keep the CTA the same size as the Spotify CTA (which shrinks to 9px). */
@media (max-width:680px){ .panel.strava .sv-cta { font-size:9px; margin-top:10px; } }

/* frame #7 = CUSTOM X "latest posts" card. Two original posts stacked, newest on
   top, divided by a thin hairline. Each tweet row mirrors X's actual UI: avatar-
   left, content-right; name + @handle + time + X-mark on one line, body, then a
   six-icon engagement row (reply / retweet / like / views / bookmark / share)
   evenly spaced across the row width — counts shown only when > 0.
   Live data from /x/latest on the worker (see knowledge/x-integration.md).
   Frame #3 reuses the same .panel.x styles with hardcoded max-char mocks so we
   can visually stress-test the layout at the worst-case (280×2 chars).

   Spacing model: each tweet is CONTENT-SIZED (flex:0 0 auto) — actions sit
   immediately under the post text rather than being pushed to the bottom of
   a flex-1 slot. THREE tweet rows pack from the top of `.x-stack`, which is
   overflow-clipped with a bottom mask-gradient: when the box is tall enough all
   three show fully (the fade falls in empty space below them); when it's shorter
   the last row(s) fade out as a "there's more" tease. This guarantees the panel
   is always visually filled and degrades 3→2→1 purely from the box height —
   no breakpoint to tune. The .panel.x element is its own size container. */
.panel.x { --x:#0b0b0c; --accent:var(--x); --muted:#6b6b70; --hair:rgba(0,0,0,.10);
  padding:14px 16px; font-family:"JetBrains Mono",ui-monospace,monospace; color:var(--ink);
  display:flex; flex-direction:column; min-height:0;
  container-type:size; container-name:x-pane; }
.panel.x .x-head { display:flex; align-items:center; gap:9px; margin-bottom:8px; flex:0 0 auto; }
.panel.x .x-dot { width:15px; height:15px; flex:0 0 auto; color:var(--x); }
.panel.x .x-lbl { font-family:"Chakra Petch",sans-serif; font-weight:600;
  text-transform:uppercase; letter-spacing:.14em; font-size:11px; color:var(--muted); }
.panel.x .x-meta { margin-left:auto; font-size:11px; color:var(--muted); }
/* The stack holds three full tweet rows packed from the top. It's overflow-clipped
   with a bottom fade: when the pane is tall enough for all three, the fade falls in
   empty space below them (no visible effect); when it's shorter, the last row fades
   out as a "there's more" peek. Adapts to the box height with no JS or breakpoints. */
.panel.x .x-stack { display:flex; flex-direction:column; flex:1 1 auto; min-height:0; overflow:hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 30px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 30px), transparent); }
.panel.x .x-tweet { display:grid; grid-template-columns:36px 1fr; gap:10px;
  flex:0 0 auto; min-height:0; align-items:start; padding:10px 0;
  position:relative; }   /* positioning context for the stretched per-row post link */
.panel.x .x-tweet:first-child { padding-top:0; }
.panel.x .x-tweet + .x-tweet  { border-top:1px solid var(--hair); }
/* Per-row click model: the whole tweet row is a stretched link to THIS post
   (.x-tweet-link, z1); the avatar opts back on top as a link to the PROFILE
   (.x-avatar-link, z2). The @handle / X-mark also sit at z2 and point at the
   post (set in render), so a click anywhere but the avatar opens the post. */
.panel.x .x-tweet-link { position:absolute; inset:0; z-index:1; border-radius:6px;
  -webkit-tap-highlight-color:transparent; }
.panel.x .x-avatar-link { display:block; position:relative; z-index:2; }
.panel.x .x-avatar { width:36px; height:36px; border-radius:999px; object-fit:cover;
  background:#e3e3e3; border:1px solid var(--hair); display:block; }
/* Content column packs top→bottom: header, body, actions. `gap` controls the
   rhythm; no flex-grow on .x-post so the actions row sits flush under the text. */
.panel.x .x-content { display:flex; flex-direction:column; min-width:0; min-height:0;
  gap:5px; overflow:hidden; }
.panel.x .x-top { display:flex; align-items:center; gap:6px;
  font-size:12px; line-height:1.2; min-width:0; }
.panel.x .x-name { font-family:"Chakra Petch",sans-serif; font-weight:700;
  letter-spacing:-.005em; color:var(--ink);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:55%; }
.panel.x .x-handle { color:var(--muted); font-size:11px; font-weight:400;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0;
  position:relative; z-index:2; }   /* stays clickable above the row's post-link */
.panel.x .x-sep, .panel.x .x-time { color:var(--muted); font-size:11px; font-weight:400; flex:0 0 auto; }
.panel.x .x-sep { opacity:.6; }
.panel.x .x-logo-link { margin-left:auto; flex:0 0 auto; display:inline-flex; color:var(--ink);
  position:relative; z-index:2; }   /* stays clickable above the row's post-link */
.panel.x .x-logo { width:13px; height:13px; opacity:.9; }
/* Line clamp gives us auto-ellipsis when text exceeds 3 lines (the "…" is
   added by -webkit-line-clamp when display:-webkit-box + overflow:hidden are
   also set). */
.panel.x .x-post { margin:0; font-size:12px; line-height:1.4; color:var(--ink);
  display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; }
/* Stats row — 6 actions distributed evenly via space-between. Count text lives
   in .x-act-n; renderX() empties it for stats that are 0 (CSS hides empties). */
.panel.x .x-actions { display:flex; justify-content:space-between; align-items:center;
  font-size:11px; color:var(--muted); font-variant-numeric:tabular-nums; flex:0 0 auto; }
.panel.x .x-act { display:inline-flex; align-items:center; gap:5px; }
.panel.x .x-act svg { width:13px; height:13px; fill:currentColor; flex:0 0 auto; }
.panel.x .x-act-n:empty { display:none; }
.panel.x:hover .x-act { color:color-mix(in srgb, var(--x) 70%, var(--muted)); }

/* No compact "hide a tweet" rule any more — the masked .x-stack handles short
   panes by fading the overflowing row, so the pane degrades gracefully (3 → 2 →
   1 visible) purely from its own height, with no breakpoint to tune. */

/* mobile: tighten everything so the stacked tweets + stats still fit on the
   small frame. Mobile anchor for frame #7 carries grow:3 (~27×21 cells). */
@media (max-width: 680px){
  .panel.x { padding:9px 10px; }
  .panel.x .x-head { margin-bottom:4px; gap:5px; }
  .panel.x .x-dot { width:13px; height:13px; }
  .panel.x .x-lbl, .panel.x .x-meta { font-size:9px; letter-spacing:.12em; }
  .panel.x .x-tweet { grid-template-columns:24px 1fr; gap:6px; padding:6px 0; }
  .panel.x .x-avatar { width:24px; height:24px; }
  .panel.x .x-content { gap:3px; }
  .panel.x .x-top { gap:4px; font-size:10px; }
  /* drop the "· time" suffix on mobile — name + handle alone barely fit. */
  .panel.x .x-sep, .panel.x .x-time { display:none; }
  .panel.x .x-name { font-size:10px; max-width:none; flex:0 1 auto; }
  .panel.x .x-handle { font-size:9px; }
  .panel.x .x-logo { width:9px; height:9px; }
  .panel.x .x-post { font-size:9px; line-height:1.3; -webkit-line-clamp:3; }
  .panel.x .x-actions { font-size:9px; }
  .panel.x .x-act { gap:3px; }
  .panel.x .x-act svg { width:10px; height:10px; }
}

/* frame #5 = CUSTOM Spotify "now playing" card. Polls the CF Worker's /now-playing
   endpoint (which proxies the Spotify Web API using a refresh_token that never expires);
   shows the currently-playing track, or — when nothing's playing — falls back to the
   most recently played one. Portrait 3:4 frame on desktop, so the layout is vertical:
   header (with "now playing" / "Xm ago" meta), big album-art square, track name, artist,
   album name, and an "Open in Spotify" CTA pinned to the bottom edge.
   Worker code: ../worker/worker.js · setup: ../../knowledge/spotify-integration.md */
.panel.spotify { --spotify:#1db954; --spotify-deep:#0f8d3f; --accent:var(--spotify);
  --muted:#6b6b70; --hair:rgba(0,0,0,.10);
  padding:14px 16px; font-family:"JetBrains Mono",ui-monospace,monospace; color:var(--ink);
  display:flex; flex-direction:column; min-height:0; }
.panel.spotify .sp-head { display:flex; align-items:center; gap:9px; margin-bottom:10px; flex:0 0 auto; }
.panel.spotify .sp-dot { width:15px; height:15px; flex:0 0 auto; color:var(--spotify);
  /* the Spotify mark is a circle, so the live-pulse ring (box-shadow, below) stays round */
  border-radius:99px; box-shadow:0 0 0 0 color-mix(in srgb, var(--spotify) 60%, transparent); }
/* a soft pulse on the dot when the track is live — makes the live-vs-historic state legible
   at a glance without needing to read the meta text */
.panel.spotify.is-playing .sp-dot { animation:sp-pulse 1.8s ease-in-out infinite; }
@keyframes sp-pulse {
  0%, 100% { box-shadow:0 0 0 0 color-mix(in srgb, var(--spotify) 60%, transparent); }
  50%      { box-shadow:0 0 0 5px color-mix(in srgb, var(--spotify) 0%, transparent); }
}
.panel.spotify .sp-lbl { font-family:"Chakra Petch",sans-serif; font-weight:600;
  text-transform:uppercase; letter-spacing:.14em; font-size:11px; color:var(--muted); }
.panel.spotify .sp-meta { margin-left:auto; font-size:11px; color:var(--muted);
  font-variant-numeric:tabular-nums; }
/* album art fills the available content width as a square (aspect-ratio:1), so it scales
   gracefully with the panel and never overflows the portrait frame. The grey background
   shows while the image hasn't loaded (or when there's no art). */
/* The art is wrapped (.sp-artbox) so the live "now playing" waveform can overlay its
   corner. The wrapper carries the square + flex-shrink; the <img> fills it. */
.panel.spotify .sp-artbox { position:relative; width:100%; aspect-ratio:1; flex:0 1 auto; min-height:0; }
/* -webkit-user-drag:none + draggable=false stop the browser starting a native image-drag
   (ghost thumbnail) when a press-drag-to-pan gesture begins on the album art. */
.panel.spotify .sp-art { display:block; width:100%; height:100%; border-radius:8px; background:#e3e3e3;
  object-fit:cover; border:1px solid var(--hair);
  box-shadow:0 8px 18px -10px rgba(0,0,0,.30);
  -webkit-user-drag:none; user-select:none; -webkit-user-select:none; }
/* Live waveform — animation #3's flowing bars at #1's corner spot. Only shown while a
   track is genuinely playing (gated on .is-playing). White bars on a translucent dark
   pill stay legible over any album cover; pointer-events:none so it never blocks the link. */
.panel.spotify .sp-eq { position:absolute; left:10px; bottom:10px; display:none;
  align-items:flex-end; gap:2.5px; height:20px; padding:5px 7px; border-radius:6px;
  background:rgba(0,0,0,.34); -webkit-backdrop-filter:blur(2px); backdrop-filter:blur(2px);
  pointer-events:none; }
.panel.spotify.is-playing .sp-eq { display:flex; }
.panel.spotify .sp-eq i { width:2.5px; border-radius:2px; background:#fff;
  animation:sp-eq 1.1s ease-in-out infinite; }
.panel.spotify .sp-eq i:nth-child(odd){ animation-duration:.9s; }
.panel.spotify .sp-eq i:nth-child(1){ animation-delay:-.2s; }
.panel.spotify .sp-eq i:nth-child(2){ animation-delay:-.5s; }
.panel.spotify .sp-eq i:nth-child(3){ animation-delay:-.1s; }
.panel.spotify .sp-eq i:nth-child(4){ animation-delay:-.7s; }
.panel.spotify .sp-eq i:nth-child(5){ animation-delay:-.35s; }
.panel.spotify .sp-eq i:nth-child(6){ animation-delay:-.6s; }
.panel.spotify .sp-eq i:nth-child(7){ animation-delay:-.15s; }
@keyframes sp-eq { 0%,100%{height:4px} 50%{height:20px} }
@keyframes sp-eq-sm { 0%,100%{height:3px} 50%{height:14px} }
/* respect reduced-motion: keep the indicator, drop the motion */
@media (prefers-reduced-motion:reduce){ .panel.spotify .sp-eq i{ animation:none; height:11px; } }
.panel.spotify .sp-info { margin-top:10px; min-width:0; flex:0 0 auto; }
.panel.spotify .sp-title { font-family:"Chakra Petch",sans-serif; font-weight:700;
  font-size:15px; line-height:1.2; letter-spacing:-.005em;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.panel.spotify .sp-artist { margin-top:3px; font-size:11.5px; color:#2a2a2d;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* album sits just under the artist as a quieter third line — muted + slightly smaller so
   it reads as secondary metadata and doesn't compete with the track/artist. */
.panel.spotify .sp-album { margin-top:2px; font-size:11px; color:var(--muted);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* margin-top:auto pins the CTA to the bottom edge of the (flex-column) panel regardless of
   how tall the art/info block ends up; padding-top keeps a min gap when the panel is full. */
.panel.spotify .sp-cta { margin-top:auto; padding-top:9px; flex:0 0 auto; align-self:flex-start;
  display:inline-flex; align-items:center; gap:6px; font-size:11px; font-weight:700;
  color:var(--spotify); text-transform:uppercase; letter-spacing:.08em; }
/* underline the label only (not the arrow) — matches the Strava CTA */
.panel.spotify .sp-cta .sp-cta-text { text-decoration:underline; text-underline-offset:3px; }
.panel.spotify .sp-cta .sp-cta-arrow { transition:transform .2s ease; }
.panel.spotify:hover .sp-cta .sp-cta-arrow { transform:translateX(3px); }
/* mobile: portrait frame is ~24×32 cells at unit 6 (~216×288 px). Tighten everything
   so the art still gets a useful chunk of the height. */
@media (max-width:680px){
  .panel.spotify { padding:9px 10px; }
  .panel.spotify .sp-head { margin-bottom:5px; gap:5px; }
  .panel.spotify .sp-dot { width:13px; height:13px; }
  .panel.spotify .sp-lbl, .panel.spotify .sp-meta { font-size:9px; letter-spacing:.12em; }
  .panel.spotify .sp-info { margin-top:6px; }
  .panel.spotify .sp-title { font-size:11px; -webkit-line-clamp:2; }
  .panel.spotify .sp-artist { font-size:9.5px; margin-top:2px; }
  .panel.spotify .sp-album { font-size:9.5px; margin-top:1px; }
  .panel.spotify .sp-cta { font-size:9px; padding-top:5px; }
  /* shrink the live waveform to match the smaller mobile art */
  .panel.spotify .sp-eq { left:7px; bottom:7px; height:14px; gap:2px; padding:3px 5px; border-radius:5px; }
  .panel.spotify .sp-eq i { width:2px; animation-name:sp-eq-sm; }
}

/* frame #2 = GREETINGS panel — the "say hi to franco" counter + in-square
   leaderboard (CF Worker + KV; identity-bound greeter records, mandatory names,
   wordlist moderation — knowledge/greetings-leaderboard-plan.md). Lives in the
   hero's SQUARE (#2, sized a touch taller than 1:1 via RATIO.gr so the frame
   breathes; #8 is the empty spare). Uses the SAME house pane chrome as every
   other panel — the base `.panel` white card (10px radius, soft shadow, hairline
   border) + the 3px RED top stripe (.panel::before) + the dot · GREETINGS · meta
   header. (The earlier BRUTALIST treatment — Anton title, red highlight slab, fat
   black borders, hard offset shadow, square corners — was removed 2026-05-29 in
   favour of full loyalty to the house style.) Three stacked views — .gr-counter
   (the say-hi form), .gr-thanks (a one-shot thank-you animation), and .gr-board
   (the leaderboard: viewer's row tinted + centred, muted "anonymous", faded
   top+bottom edges) — cross-faded by .show-thanks / .show-board. The board is
   GATED: saying hi is the only way to reach it, and once greeted the panel gets
   `.greeted` and the visitor is LOCKED to the board (no form, no "back").
   Behaviour + the offline localStorage fallback live in panels/greetings.js;
   this is the single style source for both the hero and /panels. */
/* The panel keeps the base `.panel` card chrome (we DON'T override border /
   radius / shadow here — that's what dropped the brutalist frame). `padding:0`
   because each view owns its own padding; the panel is a size CONTAINER so the
   body can scale in container units.

   FLUID SIZING (the important bit): the panel renders at a CONTINUOUS range of
   sizes — the hero packer scales it with the viewport, and it's the same
   element on /panels. Its aspect ratio is FIXED (~9:10 via RATIO.gr) at every
   breakpoint, so instead of guessing viewport @media breakpoints (which never
   line up with the actual panel size and left the toggle clipped / spacing
   broken at in-between widths), we make `.panel.greet` a size CONTAINER and size
   the whole body in `em` off a root that scales with the panel's own width
   (`font-size:5cqi` on the views). Because every dimension is proportional and
   the aspect is constant, the entire stack fits IDENTICALLY at any size — no
   breakpoints, the toggle is never cut off, spacing never breaks. */
.panel.greet { --greet:#e4002b; --greet-deep:#9c001e; --accent:var(--greet);
  --muted:#6b6b70; --hair:rgba(0,0,0,.10);
  padding:0; font-family:"JetBrains Mono",ui-monospace,monospace; color:var(--ink);
  container-type:inline-size; }
/* three stacked views overlay + cross-fade; each is transparent so the panel
   surface + its red top stripe (drawn by .panel::before behind them) stay visible.
   `font-size:5cqi` is the scaling ROOT — ~13px at the ~262px design width;
   everything below is em off it. Flow: counter → (say hi) → thanks → board.
   `.show-board` and `.show-thanks` drive which is up; greeted visitors stay on
   the board, ungreeted on the counter (the board is GATED behind saying hi). */
.panel.greet .gr-counter, .panel.greet .gr-board, .panel.greet .gr-thanks {
  position:absolute; inset:0; font-size:5cqi;
  transition:opacity .3s ease, transform .4s cubic-bezier(.2,.8,.2,1); }
.panel.greet .gr-counter, .panel.greet .gr-board {
  display:flex; flex-direction:column; padding:1em 1.07em 1.15em; }
.panel.greet .gr-counter { justify-content:space-between; }
.panel.greet .gr-board { opacity:0; transform:translateY(8px); pointer-events:none; }
.panel.greet.show-board .gr-counter { opacity:0; transform:translateY(-8px); pointer-events:none; }
.panel.greet.show-board .gr-board { opacity:1; transform:none; pointer-events:auto; }
/* the one-shot thank-you overlay — plays on the say-hi → leaderboard hand-off.
   Sits on top (z-index) over the counter; the board is preloaded behind it and
   cross-fades in when the thank-you clears. */
.panel.greet .gr-thanks { z-index:3; display:flex; align-items:center; justify-content:center;
  padding:1em; opacity:0; transform:scale(.97); pointer-events:none; }
.panel.greet.show-thanks .gr-thanks { opacity:1; transform:none; pointer-events:auto; }
.panel.greet.show-thanks .gr-counter { opacity:0; transform:translateY(-8px); pointer-events:none; }
.panel.greet .gr-thanks-in { text-align:center; display:flex; flex-direction:column; align-items:center; gap:.45em; }
.panel.greet .gr-thanks-hand { font-size:3.4em; line-height:1; display:inline-block; transform-origin:70% 80%; }
.panel.greet.show-thanks .gr-thanks-hand { animation:gr-thanks-pop .75s cubic-bezier(.2,.85,.3,1) both; }
@keyframes gr-thanks-pop {
  0%  { transform:scale(0) rotate(-30deg); }
  55% { transform:scale(1.15) rotate(12deg); }
  72% { transform:rotate(-9deg); } 86% { transform:rotate(6deg); }
  100%{ transform:scale(1) rotate(0); }
}
@media (prefers-reduced-motion:reduce){ .panel.greet.show-thanks .gr-thanks-hand { animation:none; } }
.panel.greet .gr-thanks-msg { font-family:"Chakra Petch",sans-serif; font-weight:700;
  font-size:1.5em; line-height:1.12; margin:0; color:var(--ink); }
.panel.greet .gr-thanks-sub { font-family:"JetBrains Mono",monospace; font-weight:700;
  font-size:.92em; color:var(--greet); margin:0; }
/* header (standard pane chrome) */
.panel.greet .gr-head { display:flex; align-items:center; gap:.61em; flex:0 0 auto; }
/* solid disc, so kept a touch smaller than the other panes' line-art logo marks
   (~15px) — a filled circle at their size reads heavier; ~.82em ≈ the label's own
   font-size balances its visual weight against GitHub/Strava. Still em-off-5cqi so
   it stays fluid with the pane. */
.panel.greet .gr-dot { width:.82em; height:.82em; border-radius:99px; background:var(--greet); flex:0 0 auto; }
.panel.greet .gr-lbl { font-family:"Chakra Petch",sans-serif; font-weight:600;
  text-transform:uppercase; letter-spacing:.14em; font-size:.8em; color:var(--muted); }
.panel.greet .gr-meta { margin-left:auto; font-size:.76em; color:var(--muted); }
/* the title→CTA stack. The title sits in a flexible region that grows to absorb
   the slack, so "say hi to franco" is vertically CENTRED in the band between the
   GREETINGS header and the "0 / 1,000" line, while the count/bar/name/CTA cluster
   below it. Chakra Petch (the house display face), with "hi" simply accented in
   red — no filled slab. */
.panel.greet .gr-frame { flex:1 1 auto; display:flex; flex-direction:column; min-height:0; }
.panel.greet .gr-titlewrap { flex:1 1 auto; min-height:0; display:flex; align-items:center; }
.panel.greet .gr-form { flex:0 0 auto; display:flex; flex-direction:column; gap:.62em; }
.panel.greet .gr-title { font-family:"Chakra Petch",sans-serif; font-weight:700;
  letter-spacing:-.01em; font-size:1.85em; line-height:1.12; margin:0; }
.panel.greet .gr-title em { font-style:normal; color:var(--greet); }
.panel.greet .gr-figs { display:flex; align-items:baseline; gap:.5em; }
.panel.greet .gr-cur { font-family:"Chakra Petch",sans-serif; font-weight:700; font-size:1.7em; line-height:1; }
.panel.greet .gr-of { font-size:.82em; font-weight:600; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); }
/* soft rounded progress track (matches the house aesthetic — no hard black border) */
.panel.greet .gr-progress { height:.5em; background:rgba(0,0,0,.08); border-radius:99px; overflow:hidden; }
.panel.greet .gr-progress i { display:block; height:100%; width:0; background:var(--greet);
  border-radius:99px; transition:width .9s cubic-bezier(.2,.8,.2,1); }
/* name field — soft hairline input matching the house card */
.panel.greet .gr-name { width:100%; border:1px solid var(--hair); border-radius:.6em; padding:.62em .85em;
  font-size:.9em; font-weight:500; color:var(--ink); background:#fff; font-family:"JetBrains Mono",monospace;
  transition:border-color .15s, box-shadow .15s, background .15s; }
.panel.greet .gr-name::placeholder { color:#9a9aa0; }
.panel.greet .gr-name:focus { outline:none; border-color:var(--greet);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--greet) 16%, transparent); }
.panel.greet .gr-name.gr-bad { border-color:var(--greet); background:#fff5f5; }   /* moderation reject flash */
/* the focal CTA — soft filled accent button (no hard offset shadow) */
.panel.greet .gr-cta { width:100%; font-family:"Chakra Petch",sans-serif; font-weight:700;
  font-size:1.02em; letter-spacing:.01em; background:var(--greet); color:#fff;
  border:0; border-radius:.6em; padding:.7em; cursor:pointer; white-space:nowrap;
  display:inline-flex; align-items:center; justify-content:center; gap:.4em;
  box-shadow:0 6px 16px -8px color-mix(in srgb, var(--greet) 75%, transparent);
  transition:filter .15s, box-shadow .15s, transform .06s; }
.panel.greet .gr-cta .hand { display:inline-block; animation:gr-wave 2.6s ease-in-out infinite; transform-origin:70% 80%; }
@keyframes gr-wave {
  0%, 60%, 100% { transform:rotate(0); }
  10% { transform:rotate(14deg); } 20% { transform:rotate(-8deg); }
  30% { transform:rotate(14deg); } 40% { transform:rotate(-4deg); } 50% { transform:rotate(10deg); }
}
.panel.greet .gr-cta:hover { filter:brightness(1.05); box-shadow:0 9px 20px -8px color-mix(in srgb, var(--greet) 75%, transparent); }
.panel.greet .gr-cta:active { transform:translateY(1px); }
/* disabled (name not yet typed) — quiet + clearly non-actionable */
.panel.greet .gr-cta[disabled] { background:#f1f1ef; color:#9a9aa0; cursor:default; box-shadow:none; }
.panel.greet .gr-cta[disabled] .hand { animation:none; display:none; }
/* leaderboard view (the locked end state) — the list is the viewer-centred faded
   window. No "back" affordance: once greeted, this is where you live. */
.panel.greet .gr-bframe { flex:1 1 auto; min-height:0; display:flex; flex-direction:column; }
.panel.greet .gr-bhead { display:flex; align-items:baseline; gap:.5em; flex:0 0 auto;
  font-family:"Chakra Petch",sans-serif; font-weight:700; text-transform:uppercase;
  letter-spacing:.02em; font-size:1.05em; color:var(--ink); }
.panel.greet .gr-btitle { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* running total of greetings, top-right of the heading — mono "data" voice, muted,
   in the accent colour to read as the live number. */
.panel.greet .gr-bcount { margin-left:auto; flex:0 0 auto; font-family:"JetBrains Mono",monospace;
  font-weight:500; font-size:.62em; letter-spacing:0; text-transform:none;
  color:var(--greet); font-variant-numeric:tabular-nums; white-space:nowrap; }
/* the viewer-centred window. The list is a fixed clipped window with static top/bottom
   edge-fade overlays (gradient divs painting the card's white over the clipped edges). */
.panel.greet .gr-listwrap { position:relative; flex:1 1 auto; min-height:0; display:flex; margin-top:.55em; }
/* Not scrollable: the list is a fixed window, hard-clipped to whatever rows fit.
   (A scrollable overflow container gets its own GPU layer that the hero's skewed
   plane resamples = soft row text; a plain hidden clip keeps the rows crisp.) */
.panel.greet .gr-list { flex:1 1 auto; min-height:0; overflow:hidden; }
/* Static top/bottom edge fades — gradient OVERLAYS painting the card's white over the
   clipped edges (NOT a mask on the list, which would re-layer it and re-blur the text).
   Always on: the window always clips content past the bottom, and the symmetric top
   fade frames the list into the card. */
.panel.greet .gr-fade { position:absolute; left:0; right:0; height:1.7em; pointer-events:none; }
.panel.greet .gr-fade-top { top:0; background:linear-gradient(#fff, rgba(255,255,255,0)); }
.panel.greet .gr-fade-bot { bottom:0; background:linear-gradient(rgba(255,255,255,0), #fff); }
/* Rows use Chakra Petch (the same face as the "thanks for saying hi!" heading) at the row's own
   .92em size — named 600 / anon 600-italic keep the named-vs-anonymous distinction
   while matching the heading's voice. */
.panel.greet .gr-row { display:flex; align-items:center; gap:.72em; padding:.42em .4em; font-size:.92em;
  font-family:"Chakra Petch",sans-serif; font-weight:600; border-bottom:1px solid var(--hair); border-radius:.35em; }
.panel.greet .gr-pos { font-family:"Chakra Petch",sans-serif; font-weight:700; color:var(--muted);
  font-variant-numeric:tabular-nums; min-width:2.4em; }
.panel.greet .gr-nm { flex:1 1 auto; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.panel.greet .gr-row.anon .gr-nm { color:#9a9aa0; font-style:italic; font-weight:600; }
.panel.greet .gr-row.named .gr-nm { color:var(--ink); font-weight:600; }
.panel.greet .gr-row.you { background:color-mix(in srgb, var(--greet) 9%, #fff); border-bottom-color:transparent; }
.panel.greet .gr-row.you .gr-pos { color:var(--greet); }
.panel.greet .gr-row.you .gr-nm { color:var(--greet); font-weight:700; }
/* NO @media breakpoints for the greet body: the `5cqi` em-scaling above handles
   every size continuously (narrow desktop, tablet ~1080, the tiny mobile square,
   /panels) — the panel's fixed aspect ratio means proportional scaling fits the
   same everywhere. Adding width breakpoints here is what previously clipped the
   toggle and broke spacing at in-between sizes; don't reintroduce them. */

/* frame #6 = LinkedIn profile card (replaced the old spare mock). Static — no
   live API. Recreates Franco's LinkedIn header in the panel system's own voice:
   dot+label+meta header + accent stripe (LinkedIn blue), the real cover banner,
   overlapping circular avatar, name + charcoal verified-shield badge, headline,
   location, and an experience list whose trailing roles fade out in a
   `.li-exp-peek` box (same "there's more" trick as the X pane). All imagery is
   Franco's real LinkedIn assets, self-hosted under img/linkedin/ (the licdn CDN
   URLs are signed + expiring, so we don't hotlink). MIRRORED in panels.html. */
.panel.li { --li:#0a66c2; --accent:var(--li); --muted:#6b6b70; --hair:rgba(0,0,0,.10);
  padding:0; font-family:"JetBrains Mono",ui-monospace,monospace; color:var(--ink);
  display:flex; flex-direction:column; min-height:0; overflow:hidden;
  /* size container so the card can react to its OWN height (the packer gives it
     a definite size, like the X pane). Used to drop the location line when the
     frame is short — see the @container rule below. NB: panels.html deliberately
     does NOT containerize .li (it auto-grows to content there). */
  container-type:size; container-name:li-pane; }
.panel.li .li-head { display:flex; align-items:center; gap:9px; padding:12px 16px 10px; flex:0 0 auto; }
.panel.li .li-dot { width:15px; height:15px; flex:0 0 auto; color:var(--li); }
.panel.li .li-lbl { font-family:"Chakra Petch",sans-serif; font-weight:600;
  text-transform:uppercase; letter-spacing:.14em; font-size:11px; color:var(--muted); }
.panel.li .li-meta { margin-left:auto; font-size:11px; color:var(--muted); }
/* edge-to-edge cover banner — Franco's real LinkedIn cover photo (Montreal
   skyline at night), self-hosted under img/linkedin/ so it never expires. */
.panel.li .li-banner { position:relative; flex:0 0 auto; height:24%; min-height:64px;
  background:#0b1e3a; overflow:hidden; }
.panel.li .li-banner img { width:100%; height:100%; object-fit:cover; display:block;
  -webkit-user-drag:none; user-select:none; }
.panel.li .li-body { position:relative; flex:1 1 auto; min-height:0;
  padding:0 16px 14px; display:flex; flex-direction:column; }
.panel.li .li-avatar { width:74px; height:74px; border-radius:999px; object-fit:cover;
  background:#e3e3e3; border:3px solid #fff; margin-top:-37px; flex:0 0 auto;
  box-shadow:0 6px 16px -8px rgba(0,0,0,.35); -webkit-user-drag:none; user-select:none; }
.panel.li .li-name { display:flex; align-items:center; gap:7px; margin-top:11px;
  font-family:"Chakra Petch",sans-serif; font-weight:700; font-size:21px;
  line-height:1.1; letter-spacing:-.01em; color:var(--ink); }
.panel.li .li-verified { width:18px; height:18px; flex:0 0 auto; color:#3a3a3c; }
.panel.li .li-title { font-size:13px; color:#2a2a2d; line-height:1.35; margin-top:6px; }
.panel.li .li-loc { font-size:12px; color:var(--muted); line-height:1.35; margin-top:5px; }
/* When the card is short, drop the (multi-line) location so the experience list
   gets that vertical room — maximizing how many jobs show. Threshold ~430px keeps
   the location on the roomy large-desktop frame but sheds it as #6 shrinks
   (smaller desktops, tablet, mobile). Tune the px if the cut feels off. */
@container li-pane (height < 430px) {
  .panel.li .li-loc { display:none; }
}
/* experience list — one row per role (logo + company + role + dates), newest
   first. It FILLS the remaining card height and fades its CONTENT to transparent
   over the bottom 72px — exactly the X pane's masked-peek trick. The card's white
   background stays solid (the mask is on the LIST, not the panel) and the
   avatar/name/etc. above are untouched; overflow:hidden clips rows before the fade. */
.panel.li .li-exp { flex:1 1 auto; min-height:0; overflow:hidden;
  padding-top:13px; display:flex; flex-direction:column; gap:12px;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 72px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 72px), transparent); }
.panel.li .li-exp-item { display:flex; align-items:flex-start; gap:10px; }
.panel.li .li-logo { width:34px; height:34px; border-radius:7px; flex:0 0 auto;
  object-fit:cover; border:1px solid var(--hair); margin-top:1px; -webkit-user-drag:none; }
.panel.li .li-exp-text { min-width:0; line-height:1.3; }
.panel.li .li-co-name { font-family:"Chakra Petch",sans-serif; font-weight:700;
  font-size:14px; color:var(--ink); line-height:1.2; }
.panel.li .li-role { font-size:11.5px; color:#2a2a2d; margin-top:2px; }
.panel.li .li-dates { font-size:11px; color:var(--muted); margin-top:1px;
  font-variant-numeric:tabular-nums; }
/* trailing roles — kept inert (no pointer/select) and rendered as normal rows;
   the bottom-fade now lives on `.li-exp` above (the masked, overflow-clipped
   list), so a peek-specific mask is redundant. */
.panel.li .li-exp-peek { display:flex; flex-direction:column; gap:12px;
  pointer-events:none; user-select:none; }
@media (max-width:680px){
  .panel.li .li-head { padding:9px 11px 7px; gap:5px; }
  .panel.li .li-dot { width:13px; height:13px; }
  .panel.li .li-lbl, .panel.li .li-meta { font-size:9px; letter-spacing:.12em; }
  .panel.li .li-body { padding:0 11px 10px; }
  .panel.li .li-avatar { width:56px; height:56px; margin-top:-28px; border-width:2px; }
  .panel.li .li-name { font-size:16px; margin-top:8px; }
  .panel.li .li-verified { width:14px; height:14px; }
  .panel.li .li-title { font-size:11px; }
  .panel.li .li-loc { font-size:10px; }
  .panel.li .li-logo { width:28px; height:28px; }
  .panel.li .li-co-name { font-size:12px; }
}
/* Narrow PANE shrink (container-based, not viewport). The hero can place LinkedIn
   in a small portrait box while the viewport is wide, so the @media rule above
   never fires and the type reads too large. These fire off the pane's own width
   (li-pane is a size container on the hero) and sit AFTER the @media block so they
   win there. (On /panels .li is not containerized, so these no-op — it sizes to
   content instead.) */
@container li-pane (max-width: 340px){
  .panel.li .li-name { font-size:15px; }
  .panel.li .li-title { font-size:11px; }
  .panel.li .li-loc { font-size:10px; }
  .panel.li .li-co-name { font-size:11.5px; }
  .panel.li .li-role { font-size:10px; }
  .panel.li .li-dates { font-size:9.5px; }
  .panel.li .li-logo { width:28px; height:28px; }
}
@container li-pane (max-width: 250px){
  .panel.li .li-name { font-size:13px; }
  .panel.li .li-title { font-size:10px; }
  .panel.li .li-loc { font-size:9px; }
  .panel.li .li-co-name { font-size:10.5px; }
  .panel.li .li-role { font-size:9px; }
  .panel.li .li-dates { font-size:8.5px; }
  .panel.li .li-logo { width:24px; height:24px; }
}

/* ───────────────────────── Socials (frame #3) ─────────────────────────
   STATIC pane: the standard bento header (top accent hairline + "Socials" label +
   meta) over three full-bleed vertical sub-panes, no gaps (Discord / Instagram /
   Snapchat). Each sub-pane is the platform's brand color; the profile pic sits up
   top and feathers DOWN into the panel via a gradient mask (the seam dissolves),
   with the official app-icon logo + the label seated below. The logo's squircle is
   the same brand color as its panel, so it melts in and only the mark reads. Each
   column is an <a> out. The pane is a flex column (header + body); the body
   (.so-cols) is full-bleed and reaches the side/bottom edges. `container-type:size`
   lets the label + logo scale with the frame, so the same markup reads small on the
   hero's skewed plane and large on /panels. The columns are tall + narrow, so
   object-fit:cover crops the SIDES — the per-photo focal point (--focus) keeps each
   face framed. Markup: panels/socials.js. */
.panel.so { --so:#6e56cf; --accent:var(--so); --muted:#6b6b70;
  container-type:size; container-name:so-pane;
  padding:0; overflow:hidden; display:flex; flex-direction:column; }
/* standard pane header (matches the rest of the bento): colored dot + uppercase
   label + right-aligned meta. The shared top accent hairline (::before) is kept. */
.panel.so .so-head { display:flex; align-items:center; gap:9px; flex:0 0 auto;
  padding:12px 14px 10px; }
.panel.so .so-dot { width:14px; height:14px; border-radius:5px; background:var(--so); flex:0 0 auto; }
.panel.so .so-lbl { font-family:"Chakra Petch",sans-serif; font-weight:600; letter-spacing:.14em;
  text-transform:uppercase; font-size:11px; color:var(--ink); }
.panel.so .so-meta { margin-left:auto; font-family:"JetBrains Mono",ui-monospace,monospace;
  font-size:11px; color:var(--muted); }
/* the three full-bleed sub-panes fill the body below the header (no side/bottom gaps) */
.panel.so .so-cols { display:flex; flex:1 1 auto; min-height:0; width:100%; }
.panel.so .so-col { position:relative; flex:1 1 0; min-width:0; overflow:hidden;
  display:block; text-decoration:none; -webkit-tap-highlight-color:transparent; }

/* brand fills + per-photo focal point (object-position keeps the subject framed) */
.panel.so .so-discord   { background:#5865F2; --focus:55% 20%; }
.panel.so .so-instagram { --focus:84% 16%;
  background:linear-gradient(135deg,#feda75 0%,#fa7e1e 22%,#d62976 50%,#962fbf 74%,#4f5bd5 100%); }
.panel.so .so-snapchat  { background:#FFFC00; --focus:48% 16%; }

/* profile pic up top, feathering down into the brand panel */
.panel.so .so-photo { position:absolute; left:0; right:0; top:0; width:100%; height:66%;
  object-fit:cover; object-position:var(--focus,50% 50%);
  -webkit-user-drag:none; user-select:none;
  -webkit-mask-image:linear-gradient(to bottom, #000 60%, transparent 100%);
          mask-image:linear-gradient(to bottom, #000 60%, transparent 100%); }
/* official app-icon logo seated in the panel below. Snapchat is a square PNG —
   keep its aspect ratio (the default object-fit:fill would stretch it); the
   transparent Discord/Instagram SVGs already letterbox via preserveAspectRatio. */
.panel.so .so-logo { position:absolute; left:50%; bottom:15%; transform:translateX(-50%);
  width:46%; aspect-ratio:1; -webkit-user-drag:none;
  filter:drop-shadow(0 6px 14px rgba(0,0,0,.28)); }
.panel.so .so-snapchat .so-logo { object-fit:contain; }
.panel.so .so-label { position:absolute; left:0; right:0; bottom:5%; text-align:center;
  font-family:"Chakra Petch",sans-serif; font-weight:700; text-transform:uppercase;
  letter-spacing:.14em; font-size:clamp(8px, 3.4cqh, 14px); color:#fff; }
.panel.so .so-snapchat .so-label { color:#16161a; }

/* ───────────────────────── Currently building (frame #8) ─────────────────────────
   The "what I'm building right now" pane. House pane chrome (white card, top accent
   stripe, header row) with the current project shown inside as an inset **window**
   (`.pb-win`) that nearly fills the pane but keeps a margin — so it reads as a framed
   preview, not as the whole pane.

   The window's content is the project's **native link-unfurl card** — the generic
   social `summary_large_image` shell (image · title · description · domain), NOT a
   re-style of the project's site, with every piece pulled from the project's own Open
   Graph tags. The og:image is hotlinked live so nothing here drifts. Header label says
   what the pane is; the right-hand note says "updated manually by me" (no live feed).
   The pane keeps the house top accent stripe, currently ProdBreak blue. Pairs with
   panels/prodbreak.js (pbHTML). The pane is its own size container; type/spacing scale
   with it via cqi. */
.panel.pb {
  --pb-ink:#15181f; --pb-mute:#5b6472; --pb-line:rgba(0,0,0,.10);
  --accent:#2F6BED;                    /* the "color top bar" — current project's brand (ProdBreak blue) */
  background:#fff; overflow:hidden;
  container-type:size; container-name:pb-pane;
  font-family:"Helvetica Neue",Helvetica,Arial,sans-serif; color:var(--pb-ink);
  display:flex; flex-direction:column; }

/* header — same voice as the GitHub/Socials headers (accent dot + Chakra label + mono note) */
.panel.pb .pb-head { flex:0 0 auto; display:flex; align-items:center; gap:2.4cqi;
  margin-bottom:3cqi; }
.panel.pb .pb-dot { width:clamp(8px, 2.8cqi, 12px); height:clamp(8px, 2.8cqi, 12px);
  border-radius:3px; background:var(--accent); flex:0 0 auto; }
.panel.pb .pb-lbl { font-family:"Chakra Petch",sans-serif; font-weight:600;
  text-transform:uppercase; letter-spacing:.14em; font-size:clamp(8px, 2.7cqi, 12px);
  color:var(--pb-mute); white-space:nowrap; }
.panel.pb .pb-note { margin-left:auto; font-family:"JetBrains Mono",ui-monospace,monospace;
  font-size:clamp(6.5px, 2.2cqi, 10.5px); letter-spacing:.02em; color:var(--pb-mute);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

/* the inset window — almost fills the rest of the pane, with a border + soft lift */
.panel.pb .pb-win { flex:1 1 auto; min-height:0; display:flex; flex-direction:column;
  border:1px solid var(--pb-line); border-radius:8px; overflow:hidden; background:#fff;
  box-shadow:0 8px 18px -12px rgba(0,0,0,.28); }
/* the og:image — large preview thumbnail, like the social card */
.panel.pb .pb-img { flex:0 0 auto; height:58%; background:#1B2336; overflow:hidden; }
.panel.pb .pb-img img { width:100%; height:100%; object-fit:cover; object-position:center;
  display:block; }
/* meta rail — title / description / domain, the generic unfurl footer */
.panel.pb .pb-info { flex:1 1 auto; min-height:0; display:flex; flex-direction:column;
  justify-content:center; gap:1.2cqi; padding:2.8cqi 3.6cqi; border-top:1px solid var(--pb-line); }
.panel.pb .pb-title { font-weight:700; font-size:clamp(10px, 4cqi, 17px); line-height:1.25;
  letter-spacing:-.01em; color:var(--pb-ink);
  display:-webkit-box; -webkit-line-clamp:1; -webkit-box-orient:vertical; overflow:hidden; }
.panel.pb .pb-desc { font-size:clamp(8.5px, 3cqi, 13px); line-height:1.4; color:var(--pb-mute);
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.panel.pb .pb-domain { display:flex; align-items:center; gap:1.6cqi; margin-top:.4cqi;
  font-size:clamp(7.5px, 2.7cqi, 12px); color:var(--pb-mute); }
.panel.pb .pb-favicon { width:clamp(10px, 3.2cqi, 15px); height:clamp(10px, 3.2cqi, 15px);
  border-radius:3px; flex:0 0 auto; }

/* short pane → drop the description so the image + title + domain stay readable */
@container pb-pane (max-height: 165px) {
  .panel.pb .pb-img { height:52%; }
  .panel.pb .pb-desc { display:none; }
}
