/* ════════════════════════════════════════════════════════════════════════════
   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 panels — placeholders kept so every bento still shows its top
   accent stripe. #3 = Icons; #8 = the empty spare freed when greetings moved into
   the square (the #2 ↔ #8 role swap, 2026-05-29 — greetings is now a LIVE panel
   at #2, so the old `.panel.m2` rule is gone). #5 Spotify and #6 LinkedIn are
   live panels (see below). */
.panel.m3 { --accent:#6e56cf; }   /* purple — icons */
.panel.m8 { --accent:#12a594; }   /* teal   — reserved spare */

/* 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; }
.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; }
/* 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; }
.panel.greet .gr-dot { width:1.07em; height:1.07em; 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; }
