/* Skeleton stylesheet for the generated site. Layout and hooks only — the real styling
   pass comes later. Palette: eggshell void, near-black type, one accent, and a per-station
   tint carried by the --tint custom property (set inline by the generator). */

/* ---- site voice: Fraunces (serif display/headings) + Overused Grotesk (sans body/flow text) ----
   Variable-font latin subsets, self-hosted (offline/no-CDN like everything else).
   Overused Grotesk is SIL OFL 1.1 (bundling/self-hosting permitted). */
@font-face {
  font-family: "Fraunces";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("assets/fonts/fraunces-latin-var.woff2") format("woff2");
}
@font-face {
  font-family: "Overused Grotesk";
  font-style: normal;
  font-weight: 300 900;
  font-display: swap;
  src: url("assets/fonts/overused-grotesk-var.woff2") format("woff2");
}

:root {
  --bg: #f4f1ea;      /* eggshell */
  --ink: #1a1a1a;     /* near-black */
  --accent: #c96a2b;  /* the one accent */
  --tint: transparent;
  /* Two-font system (Georg 2026-07-11): the whole site is Fraunces (display/serif) + Overused
     Grotesk (everything else). The old "mono voice" (kickers, labels, badges, counters, button
     labels) is now Overused Grotesk too — its uppercase + letter-spacing + weight keep the
     label texture without a third typeface. --mono kept as an alias so the ~40 label rules
     don't each need touching. */
  --mono: "Overused Grotesk", system-ui, sans-serif;
  /* Every button on the site speaks in the serif (Georg 2026-10-01: "all buttons, also the
     ones on the starting page") — the pills, the brush-stroke "View project", the guestbook's
     Send. One token, so the button voice is tuned in one place. */
  --btn-font: "Fraunces", Georgia, serif;
  /* One size for every big section heading down the pages (Georg 2026-09-29: "make sure
     headings have similar sizes") — the "I make" lead, the bench, the panels, the studio map
     and the table cards all read it, so they stay in one family when it's tuned. */
  --section-h: clamp(2.4rem, 1.2rem + 3.4vw, 4.25rem);
}

* { box-sizing: border-box; }
/* clip (not hidden) so the hero's 100vw full-bleed breakout can't cause a horizontal
   scrollbar on Windows without breaking position:sticky descendants */
html { overflow-x: clip; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: "Overused Grotesk", system-ui, sans-serif;
  line-height: 1.5;
}
a { color: inherit; }
img, video { max-width: 100%; display: block; }

/* The front page plays more like a game than a document: dragging over the meadow, a table or
   a heading must not smear a blue text selection across the layout. So its text can't be
   selected — except the footer (an address someone may want to copy) and form fields. user-select
   passes down to descendants, so this one rule on <body class="page-home"> (pageShell
   bodyClass) covers the whole page. */
.page-home { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
.page-home .site-footer,
.page-home input,
.page-home textarea { -webkit-user-select: text; user-select: text; }

/* ---- headings: bold editorial serif ---------------------------------------
   Fraunces carries the display voice against the Overused Grotesk body; tracking is
   softer than the old system stack because serifs need less. */
h1, h2, h3, h4 {
  font-family: "Fraunces", Georgia, serif;
  font-weight: 800;
  letter-spacing: -0.01em;
  line-height: 1.08;
  text-wrap: balance;
}
h1 { font-size: clamp(2.2rem, 5vw, 3.4rem); margin: 0 0 1rem; }
h2 { font-size: clamp(1.5rem, 3vw, 2.1rem); margin: 0 0 0.75rem; }
h3 { font-size: 1.15rem; letter-spacing: -0.01em; margin: 0.5rem 0 0.25rem; }

/* Page-title h1s carry a short accent bar; body-prose headings opt out. */
h1::after {
  content: ""; display: block; width: 2.5rem; height: 3px;
  margin-top: 0.6rem; background: var(--accent); border-radius: 2px;
}
.prose h1::after { display: none; }

/* Embedded HTML in prose markdown (videos, iframes) never breaks the column. */
.prose iframe, .prose video, .prose embed, .prose object { max-width: 100%; }
.prose iframe { border: 0; }

/* Mono "kicker" label above big headings — the spec-line idiom. */
.kicker, .station-kicker {
  margin: 0 0 0.4rem;
  font-family: var(--mono); font-size: 0.72rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.1em; color: rgba(0, 0, 0, 0.5);
}

/* The fold's hero statement — the single loudest voice on the site. */
.h-display {
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(2rem, 6vw, 3.6rem); font-weight: 800;
  letter-spacing: -0.015em; line-height: 1.02;
}

/* Skip-to-content: off-screen until focused, then pinned top-left. */
.skip-link {
  position: absolute; left: 0.5rem; top: -3rem; z-index: 100;
  background: var(--ink); color: var(--bg);
  padding: 0.5rem 0.9rem; border-radius: 0 0 0.3rem 0.3rem; text-decoration: none;
  transition: top 0.15s ease;
}
.skip-link:focus { top: 0; }

/* ---- global frame: header nav (html.js header + assets/site.js burger) ---------------
   A chromeless row of uppercase text links floating over the page top — no background,
   border, or shadow (the "no bg color" rule). Ink on the eggshell subpages, white over the
   home hero's sky. Below 768px (and only with JS) the links fold into a small eggshell
   dropdown behind a two-line burger; without JS they just stay inline. */
.site-header {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 99;
  padding: 1.5rem clamp(1.25rem, 6vw, 5rem);
}
body:has(.hero) .site-header {
  color: #fff;
  text-shadow: 0 1px 12px rgba(31, 63, 110, 0.4);
}
.site-nav {
  position: relative;
  display: flex;
  align-items: center;
  gap: 1rem;
}
.site-nav-links {
  display: flex;
  gap: clamp(1.75rem, 6vw, 4.5rem);
  margin: 0;
  padding: 0;
  list-style: none;
}
.site-nav-links a {
  font-family: var(--mono);
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.45em;
}
.site-nav-links a:hover,
.site-nav-links a:focus-visible,
.site-nav-links a[aria-current="page"] { text-decoration-line: underline; }
.nav-burger {
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 2.75rem; height: 2.75rem;
  margin: -0.6rem 0 -0.6rem -0.6rem; /* 44px hit area, lines still flush with the gutter */
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
}
.nav-burger-line {
  width: 26px; height: 2px;
  border-radius: 1px;
  background: currentColor;
  transition: transform 0.25s ease;
}
body:has(.hero) .nav-burger-line { box-shadow: 0 1px 8px rgba(31, 63, 110, 0.25); }
.site-nav.open .nav-burger-line:first-child { transform: translateY(4px) rotate(45deg); }
.site-nav.open .nav-burger-line:last-child { transform: translateY(-4px) rotate(-45deg); }

/* Language switcher pill (header + footer): tiny flag + DE/EN label in a quiet outline,
   pushed to the far end of the header row. */
.lang-switch {
  display: inline-flex;
  align-items: center;
  align-self: center;
  gap: 0.4rem;
  padding: 0.3rem 0.6rem;
  margin-left: auto;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink);
  border: 1px solid color-mix(in srgb, var(--ink) 25%, transparent);
  border-radius: 0.4rem;
  text-decoration: none;
  transition: border-color 0.3s ease, background-color 0.3s ease;
}
.lang-switch:hover { border-color: var(--ink); background: color-mix(in srgb, var(--ink) 6%, transparent); }
.lang-flag { width: 1.05em; height: 0.75em; border-radius: 2px; display: block; flex: none; box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink) 15%, transparent); }
.lang-switch-foot { margin-left: 0; margin-top: 0.35rem; align-self: flex-start; }
@media (max-width: 768px) {
  .site-header { padding: 1.1rem 1.25rem; }
  html.js .nav-burger { display: flex; }
  html.js .site-nav-links {
    position: absolute;
    top: calc(100% + 0.6rem); left: 0;
    flex-direction: column;
    gap: 0;
    min-width: 11rem;
    padding: 0.35rem;
    background: var(--bg);
    color: var(--ink);
    text-shadow: none;
    border-radius: 0.6rem;
    box-shadow: 0 10px 30px rgba(26, 26, 26, 0.18);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-0.4rem);
    transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0.2s;
  }
  html.js .site-nav.open .site-nav-links {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: opacity 0.2s ease, transform 0.2s ease;
  }
  html.js .site-nav-links a { display: block; padding: 0.7rem 0.85rem; }
}

/* Top padding clears the floating header nav (plus the breathing room the hero sky swallows). */
.site-main { max-width: 60rem; margin: 0 auto; padding: 6.5rem 2rem 2rem; }

/* Closing contact call-to-action — the page's last in-flow section before the footer. Its
   own full-width band between two edge-to-edge rules (Georg 2026-09-26: "more separated");
   the padding re-centres the copy in a 34rem measure. */
.contact-cta {
  width: 100vw; margin: 7rem 0 2rem calc(50% - 50vw);
  padding: 5rem max(1.25rem, calc(50vw - 17rem));
  border-top: 1px solid var(--ink); border-bottom: 1px solid var(--ink);
  text-align: center;
}
.contact-cta-line {
  margin: 0 0 1.5rem;
  font-family: "Fraunces", Georgia, serif; font-weight: 800;
  font-size: clamp(1.4rem, 2.6vw, 1.9rem); line-height: 1.25; letter-spacing: -0.01em;
  text-wrap: balance;
}
.contact-cta-btn {
  display: inline-block;
  padding: 0.7rem 1.4rem;
  background: var(--ink); color: var(--bg);
  border-radius: 999px; text-decoration: none;
  font-family: var(--btn-font); font-weight: 600;
  transition: background 0.15s ease;
}
.contact-cta-btn:hover { background: #333; }

/* The contact-form block (Web3Forms): one column of labelled fields in a 34rem measure. */
.contact-form { max-width: 34rem; margin: 3rem 0; display: grid; gap: 1rem; }
.contact-form h2 { margin: 0 0 0.25rem; }
.contact-form label { display: grid; gap: 0.35rem; font-weight: 600; }
.contact-form input[type="text"], .contact-form input[type="email"], .contact-form textarea {
  font: inherit; font-weight: 400; color: var(--ink); background: #fffdf8;
  padding: 0.65rem 0.8rem; border: 1px solid var(--ink); border-radius: 6px;
}
.contact-form textarea { resize: vertical; min-height: 8rem; }
.contact-form input:focus-visible, .contact-form textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.contact-form .contact-cta-btn { justify-self: start; border: 0; font: inherit; font-family: var(--btn-font); font-weight: 600; cursor: pointer; }
.contact-form .contact-cta-btn:disabled { opacity: 0.6; cursor: progress; }
.contact-form-note { margin: 0; font-size: 0.9rem; opacity: 0.8; }
.contact-form-status { margin: 0; font-weight: 600; }
.contact-form-status.is-error { color: #a33a1d; }
.contact-form-trap { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }

/* /nfc/ — the business card's landing page. Built for the phone that just tapped the card:
   one centred column, full-width buttons big enough for a thumb. */
.nfc-card {
  max-width: 26rem; margin: 0 auto;
  text-align: center;
}
.nfc-portrait {
  width: 9rem; height: 9rem; margin: 0 auto 1.25rem;
  border-radius: 50%; object-fit: cover;
  border: 3px solid var(--bg); box-shadow: 0 0 0 1px var(--ink);
}
.nfc-kicker { color: var(--accent); }
.nfc-name { margin-bottom: 0.4rem; }
.nfc-name::after { margin-left: auto; margin-right: auto; }
.nfc-sub {
  margin: 0.6rem 0 0.25rem;
  font-family: "Fraunces", Georgia, serif; font-style: italic; font-size: 1.2rem;
}
.nfc-tagline { margin: 0; color: rgba(0, 0, 0, 0.65); text-wrap: balance; }
.nfc-actions { display: grid; gap: 0.6rem; margin: 1.75rem 0 2.25rem; }
.nfc-btn {
  display: block; width: 100%;
  padding: 0.85rem 1.25rem;
  font: inherit; font-family: var(--btn-font); font-weight: 600; text-align: center; text-decoration: none;
  color: var(--ink); background: transparent;
  border: 1px solid var(--ink); border-radius: 999px;
  cursor: pointer;
  overflow-wrap: anywhere;
  transition: background 0.15s ease, color 0.15s ease;
}
.nfc-btn:hover, .nfc-btn:focus-visible { background: rgba(0, 0, 0, 0.06); }
.nfc-btn--primary { background: var(--ink); color: var(--bg); }
.nfc-btn--primary:hover, .nfc-btn--primary:focus-visible { background: #333; }
.nfc-links-h {
  margin: 0 0 0.5rem;
  font-family: var(--mono); font-size: 0.72rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.1em; color: rgba(0, 0, 0, 0.5);
}
.nfc-links { list-style: none; margin: 0; padding: 0; border-top: 1px solid rgba(0, 0, 0, 0.15); }
.nfc-link {
  display: flex; justify-content: space-between; align-items: center;
  padding: 0.85rem 0.25rem;
  font-family: "Fraunces", Georgia, serif; font-weight: 800; font-size: 1.2rem;
  text-decoration: none;
  border-bottom: 1px solid rgba(0, 0, 0, 0.15);
}
.nfc-link:hover .nfc-link-arrow, .nfc-link:focus-visible .nfc-link-arrow { color: var(--accent); transform: translate(2px, -2px); }
.nfc-link-arrow { font-family: var(--mono); font-weight: 400; transition: transform 0.15s ease, color 0.15s ease; }
.nfc-more { margin: 2.5rem 0 0; text-align: center; }

/* Footer: three zoned columns (identity · navigate/social media · log/legal), not a link pile —
   a dark full-width band with light text (Georg 2026-09-28), painted as a watercolour wash on
   every page (2026-09-29). The wash (assets/about/footer-wash.webp) is the band's texture
   (cover — never squashed); the mask is its ragged top and bottom edges, cut out as fixed-
   height strips (footer-edge-*.webp) around a solid middle, so the edges stay the same size
   however tall the footer grows, and the paddings keep the links off them. */
.site-footer {
  --foot-paper: #f1ebdf; /* under the wash, as on the painted sheet (it is ~80% opaque) */
  --foot-ink: #f3eee3;
  --foot-dim: rgba(243, 238, 227, 0.62);
  position: relative; isolation: isolate;
  margin-top: 6rem; padding: 5.5rem 2rem 5.25rem;
  color: var(--foot-ink);
}
.site-footer::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: var(--foot-paper) url("assets/about/footer-wash.webp") center / cover no-repeat;
  -webkit-mask:
    url("assets/about/footer-edge-top.webp") top / 100% 4.5rem no-repeat,
    linear-gradient(#000, #000) center / 100% calc(100% - 8.5rem) no-repeat,
    url("assets/about/footer-edge-bottom.webp") bottom / 100% 5rem no-repeat;
  mask:
    url("assets/about/footer-edge-top.webp") top / 100% 4.5rem no-repeat,
    linear-gradient(#000, #000) center / 100% calc(100% - 8.5rem) no-repeat,
    url("assets/about/footer-edge-bottom.webp") bottom / 100% 5rem no-repeat;
}
.foot-grid {
  max-width: 60rem; margin: 0 auto;
  display: grid; grid-template-columns: 1.6fr 1fr 1fr; gap: 2rem 2.5rem;
}
.foot-zone, .foot-legal { display: flex; flex-direction: column; align-items: flex-start; gap: 0.4rem; }
.site-footer a { color: var(--foot-ink); text-decoration: none; }
.site-footer a:hover { text-decoration: underline; text-decoration-color: var(--accent); }
.foot-wordmark { font-weight: 800; font-size: 1.15rem; letter-spacing: -0.01em; }
.foot-tagline { margin: 0.4rem 0 0; max-width: 22em; font-size: 0.9rem; color: var(--foot-dim); }
.foot-location {
  margin: 0.5rem 0 0;
  font-family: var(--mono); font-size: 0.7rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em; color: var(--foot-dim);
}
/* golden-feather tally — deliberately tiny; [hidden] until feather-collect.js fills it */
.foot-feathers { margin: 0.4rem 0 0; font-family: var(--mono); font-size: 0.68rem; color: var(--foot-dim); }
.foot-h {
  margin: 0 0 0.2rem;
  font-family: var(--mono); font-size: 0.7rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em; color: var(--foot-dim);
}
.foot-h-sub { margin-top: 0.9rem; }
.foot-latest { margin: 0 0 0.3rem; font-size: 0.9rem; }
.foot-latest-tag {
  font-family: var(--mono); font-size: 0.7rem; text-transform: uppercase;
  letter-spacing: 0.06em; color: #f0a56a; margin-right: 0.4rem;
}
.foot-latest a { text-decoration: none; }
.foot-latest a:hover { text-decoration: underline; }
.foot-latest-when { display: block; font-family: var(--mono); font-size: 0.7rem; color: var(--foot-dim); margin-top: 0.15rem; }
.site-footer .lang-switch { color: var(--foot-ink); border-color: var(--foot-dim); }

/* ---- labeling layer ---- */
.attempt { font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.08em; }
.status-badge { font-family: var(--mono); font-size: 0.75rem; }
.station-chip {
  display: inline-block;
  padding: 0.15rem 0.6rem;
  background: var(--tint);
  text-decoration: none;
  font-size: 0.9rem;
}
.station-chip .feather { color: var(--accent); }

/* ---- front page hero: full-bleed painterly 3D scene (assets/hero-scene.js) ---- */
/* Breaks out of .site-main's 60rem column to true full width. The CSS sky gradient is the
   base layer everywhere: the fallback while WebGL loads (or fails) and the whole hero on
   mobile, where the canvas is display:none. */
.hero {
  position: relative;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-top: -6.5rem; /* swallow .site-main's top padding so the sky runs under the floating nav */
  height: clamp(30rem, 88vh, 60rem);
  overflow: hidden;
  background: linear-gradient(to bottom, #3f7fd0 0%, #cfe4f5 62%, #5e875e 62.5%, #47694a 100%);
}
/* On a PC the meadow opens compact, so more of the page shows under it; the first click into
   it (hero-boot.js adds .is-expanded) grows it to the full height above for the rest of the
   visit (Georg 2026-10-02). Phones and tablets keep their own heights further down. */
@media (min-width: 40.01rem) and (hover: hover) and (pointer: fine) {
  .hero:not(.is-expanded) { height: clamp(26rem, 60vh, 40rem); }
  .hero { transition: height 0.55s cubic-bezier(0.3, 0.7, 0.2, 1); }
}
@media (min-width: 40.01rem) and (hover: hover) and (pointer: fine) and (prefers-reduced-motion: reduce) {
  .hero { transition: none; }
}
.hero-scene { position: absolute; inset: 0; }
/* Loading cover. The mount blocks the main thread for a beat (shader compiles, grass
   scattering, GLB decode) — during that block NOTHING driven by JS can paint, so the
   cover is pure CSS painted before any script runs:
   ::before = a real screenshot of the meadow (66KB jpg, instant) with a dark bar track
   baked in as a background layer; ::after = the loading bar, animated via a COMPOSITOR
   transform (will-change) so it keeps creeping even while JavaScript is frozen. The
   honest byte readout (.scene-load) overlays whenever the thread is free. Everything
   fades the moment the scene reports live; a warm remount shows it only for a blink. */
.hero-scene::before {
  content: "";
  position: absolute; inset: 0; z-index: 3;
  background:
    linear-gradient(rgba(24, 22, 19, 0.4), rgba(24, 22, 19, 0.4)) center 62% / 18rem 9px no-repeat,
    #cfe4f5 url("assets/textures/hero-poster.jpg") center / cover no-repeat;
  transition: opacity 0.7s ease;
  pointer-events: none; /* faded to 0 but still on top — must never eat canvas clicks (items, shelf, feathers) */
}
.hero-scene[data-hero-state="live"]::before { opacity: 0; }
/* Visitors who get a bird open on the painted title (hero-title-art.js, title-art-math.js): the
   cover is that painting, flattened, on a box 2% bigger than the hero and placed like the live
   set (cover, focus 50% 48% — the portrait plate 50% 55% at or under 4:5), so the set takes over
   from it without a jump. No fake loading bar on the painting. The media test is hero-boot's
   canPlay(); the aspect test is plateFor()'s. */
@media (prefers-reduced-motion: no-preference) and (hover: hover), (prefers-reduced-motion: no-preference) and (hover: none) and (pointer: coarse) {
  .hero { background: linear-gradient(to bottom, #8a78a6 0%, #e8a27e 50%, #8c8a46 52%, #4f5a2c 100%); }
  .hero-scene::before {
    inset: -2%;
    background: #6d5a72 url("assets/title/poster-wide.webp") 50% 48% / cover no-repeat;
  }
  .hero-scene[data-hero-state="idle"]::after { display: none; }
}
@media (prefers-reduced-motion: no-preference) and (hover: hover) and (max-aspect-ratio: 4/5), (prefers-reduced-motion: no-preference) and (hover: none) and (pointer: coarse) and (max-aspect-ratio: 4/5) {
  .hero-scene::before { background-image: url("assets/title/poster-tall.webp"); background-position: 50% 55%; }
}
.hero-scene::after {
  content: "";
  position: absolute; left: 50%; top: 62%; z-index: 4;
  width: 18rem; height: 9px; margin-left: -9rem; margin-top: -4.5px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.92);
  transform-origin: left center;
  will-change: transform; /* own compositor layer — survives main-thread blocks */
  display: none;
}
.hero-scene[data-hero-state="idle"]::after {
  display: block;
  animation: hero-fakeload 6s cubic-bezier(0.3, 0.75, 0.25, 1) forwards;
}
@keyframes hero-fakeload {
  from { transform: scaleX(0.04); }
  to { transform: scaleX(0.97); }
}
/* Download readout for the 3D scenes (hero + vignettes + bench): a centered mono pill with
   a slim progress track showing how many MB are still streaming in. The hosts create/update
   it from model-loader's byte progress (writing --scene-load 0..1 for the track) and hide it
   themselves when loading ends (cache-warm mounts never show it — no progress events fire). */
.scene-load {
  position: absolute; left: 50%; top: 50%; z-index: 4;
  transform: translate(-50%, -50%);
  display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
  padding: 0.75rem 1.4rem 0.85rem;
  background: rgba(24, 22, 19, 0.72); color: #f2ecdf;
  border-radius: 0.8rem;
  font-family: var(--mono); font-size: 0.9rem; letter-spacing: 0.06em;
  pointer-events: none; white-space: nowrap;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.25);
}
/* The slim fill track under the label — width follows --scene-load (0..1). */
.scene-load::after {
  content: "";
  width: 15rem; height: 5px; border-radius: 999px;
  background: linear-gradient(to right,
    rgba(255, 255, 255, 0.85) calc(var(--scene-load, 0) * 100%),
    rgba(255, 255, 255, 0.18) calc(var(--scene-load, 0) * 100%));
  transition: background 0.25s ease-out;
}
.scene-load[hidden] { display: none; }
/* The renderer draws at a reduced buffer size (painterly pass is per-pixel expensive);
   CSS stretches the canvas back to full size — invisible under the smear. */
.hero-scene .hero-canvas {
  position: absolute; inset: 0;
  width: 100% !important; height: 100% !important;
  display: block; touch-action: none;
}
/* Title scene (hero-intro.js / hero-boot.js): a light live meadow that fades in over the
   poster within a beat of the load while the full painterly world builds hidden underneath;
   once that's ready it dissolves away by itself. A SIBLING right after .hero-scene (outside
   its aria-hidden, so the loading status stays readable). z5: above the meadow's own overlays
   (z3–4) through the dissolve, below .hero-copy (also z5, later in the DOM) so the name
   reads on top. Keep the .is-leaving duration in sync with FADE_MS in hero-intro.js. */
.hero-intro {
  position: absolute; inset: 0; z-index: 5;
  opacity: 0; transition: opacity 0.6s ease;
}
.hero-intro.is-shown { opacity: 1; }
.hero-intro.is-leaving { opacity: 0; transition: opacity 0.9s ease; pointer-events: none; }
.hero-intro-canvas {
  position: absolute; inset: 0;
  width: 100% !important; height: 100% !important;
  display: block;
}
/* The painted title (hero-title-art.js): its own canvas over the title scene's, fading in over
   the identical poster once its layers are in, and out (0.9s, FADE_MS there) as "▶ Play" pushes
   into the place. Until it's in, the title scene's canvas stays hidden so the poster shows. */
.hero-title-art {
  position: absolute; inset: 0;
  width: 100% !important; height: 100% !important;
  display: block; pointer-events: none;
  opacity: 0; transition: opacity 0.5s ease;
}
.hero-title-art.is-ready { opacity: 1; }
.hero-title-art.is-leaving { opacity: 0; transition: opacity 0.9s ease 0.15s; }
.hero-intro.is-art-pending .hero-intro-canvas { visibility: hidden; }
/* Over the sunset the name's shadow turns warm. */
.hero.has-title-art .hero-name { text-shadow: 0 2px 26px rgba(70, 30, 40, 0.45); }
.hero.has-title-art .hero-subline,
.hero.has-title-art .hero-location { text-shadow: 0 1px 12px rgba(70, 30, 40, 0.55); }
/* While the intro covers the building meadow, the meadow's DOM overlays (HUD, emote bar, ⚙,
   name tags, the MB pill…) and its canvas stay out of sight — they surface under the dissolve. */
.hero-scene[data-hero-veiled] > * { visibility: hidden; }
/* The loading pill sits where the poster's loading bar was (62%), so the bar reads as turning
   into it: a dark pill with the download track while the meadow builds; "▶ Play" once it's
   ready, right before the dissolve — clickable then (it enters at once; it would anyway). */
.hero-play {
  position: absolute; left: 50%; top: 62%;
  transform: translate(-50%, -50%);
  display: flex; flex-direction: column; align-items: center; gap: 0.55rem;
  min-width: 17rem;
  padding: 0.8rem 1.6rem 0.9rem;
  border: 0; border-radius: 0.9rem;
  background: rgba(24, 22, 19, 0.72); color: #f2ecdf;
  font-family: var(--mono); font-size: 0.9rem; font-weight: 600; line-height: 1.2;
  letter-spacing: 0.08em; font-variant-numeric: tabular-nums; white-space: nowrap;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.25);
  pointer-events: none;
}
.hero-play-track {
  position: relative; overflow: hidden;
  width: 15rem; height: 5px; border-radius: 999px;
  background: rgba(255, 255, 255, 0.18);
}
.hero-play-track::after {
  content: "";
  position: absolute; inset: 0;
  background: rgba(255, 255, 255, 0.85);
  transform-origin: left center;
  transform: scaleX(var(--intro-progress, 0));
  transition: transform 0.35s ease-out;
}
.hero-play[data-state="ready"], .hero-play[data-state="queued"] { pointer-events: auto; cursor: pointer; }
.hero-play[data-state="ready"]:hover, .hero-play[data-state="queued"]:hover { background: rgba(24, 22, 19, 0.86); }
.hero-play:disabled { opacity: 1; } /* status while loading — never greyed out */
.hero-play[data-state="failed"] { display: none; }
/* The quiet play-data line under the pill (hero-intro.js): small, soft, readable on the sky. */
.hero-play-note {
  position: absolute; left: 50%; top: calc(62% + 3.1rem);
  transform: translateX(-50%);
  margin: 0; max-width: min(92%, 30rem);
  font-size: 0.72rem; line-height: 1.35; text-align: center;
  color: rgba(255, 255, 255, 0.82); text-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
}
.hero-play-note a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.hero-intro.is-static .hero-play-note { display: none; }
@media (prefers-reduced-motion: reduce) {
  .hero-intro, .hero-intro.is-leaving { transition-duration: 0.15s; }
}
/* The bird picker (hero-select.js): the title scene opens on the level's stump with a bird on
   it. Its controls sit on points hero-intro projects from the 3D shot every frame — the line
   over the bird's head (--pick-head-x/y), the arrow signs beside the stump (--pick-left-x /
   --pick-right-x at --pick-mid-y) and the pill + dots under its foot (--pick-base-y) — so they
   stay on the stump at any screen shape. Sideways swipes switch birds; vertical ones scroll. */
.hero-intro.is-select { touch-action: pan-y; }
.hero-pick { position: absolute; inset: 0; pointer-events: none; }
.hero-pick button { pointer-events: auto; }
.hero-pick-line {
  position: absolute; left: var(--pick-head-x, 50%); top: var(--pick-head-y, 40%);
  transform: translate(-50%, calc(-100% - 0.4rem));
  margin: 0; max-width: min(92vw, 34rem); width: max-content;
  font: 700 clamp(1.45rem, 2.6vw, 2.05rem)/1.05 "Caveat", "Segoe Print", "Comic Sans MS", cursive;
  color: #fff; text-align: center; text-wrap: balance;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.45), 0 0 18px rgba(0, 0, 0, 0.25);
  transition: opacity 0.4s ease;
}
.hero-pick-text { font-weight: 600; }
/* The arrows are painted wooden signs on a post, pointing away from the stump. */
.hero-pick-arrow {
  position: absolute; top: var(--pick-mid-y, 70%);
  width: 4.4rem; height: 4.6rem; padding: 0; border: 0; background: none; cursor: pointer;
  transition: opacity 0.35s ease, transform 0.15s ease;
}
.hero-pick-arrow.is-prev { left: var(--pick-left-x, 35%); transform: translate(calc(-100% - 1.6rem), -60%); }
.hero-pick-arrow.is-next { left: var(--pick-right-x, 65%); transform: translate(1.6rem, -60%); }
.hero-pick-arrow::after { /* the post */
  content: ""; position: absolute; left: 50%; top: 1.1rem; bottom: -0.6rem; width: 0.42rem;
  transform: translateX(-50%); border-radius: 2px;
  background: linear-gradient(90deg, #6b4a2c, #8a6440 55%, #5b3d22);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
}
.hero-pick-plank {
  position: absolute; left: 0; right: 0; top: 0.2rem; height: 1.75rem; z-index: 1;
  background: linear-gradient(180deg, #c99a63 0%, #b07f4b 55%, #966537 100%);
  box-shadow: inset 0 -2px 0 rgba(70, 40, 15, 0.35), 0 3px 6px rgba(0, 0, 0, 0.28);
  clip-path: polygon(0 0, 78% 0, 100% 50%, 78% 100%, 0 100%, 6% 50%);
}
.hero-pick-arrow.is-prev .hero-pick-plank { clip-path: polygon(22% 0, 100% 0, 94% 50%, 100% 100%, 22% 100%, 0 50%); }
.hero-pick-arrow.is-prev:hover { transform: translate(calc(-100% - 1.85rem), -60%); }
.hero-pick-arrow.is-next:hover { transform: translate(1.85rem, -60%); }
.hero-pick-arrow:focus-visible { outline: none; }
.hero-pick-arrow:focus-visible .hero-pick-plank { filter: brightness(1.15); box-shadow: 0 0 0 3px #fff; }
.hero-pick-dots {
  position: absolute; left: 50%; top: calc(var(--pick-base-y, 80%) + 2.4rem);
  transform: translateX(-50%);
  display: flex; gap: 0.55rem; transition: opacity 0.35s ease;
}
.hero-pick-dot {
  width: 0.7rem; height: 0.7rem; padding: 0; border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(24, 22, 19, 0.55); box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.55);
}
.hero-pick-dot[aria-pressed="true"] { background: rgba(24, 22, 19, 0.9); transform: scale(1.25); }
/* A bird already flying in the meadow: an empty ring with a stroke through it, not choosable. */
.hero-pick-dot.is-taken {
  cursor: not-allowed; background: transparent; box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.4);
  background-image: linear-gradient(135deg, transparent 44%, rgba(255, 255, 255, 0.75) 46%, rgba(255, 255, 255, 0.75) 54%, transparent 56%);
}
.hero-pick-note {
  position: absolute; left: var(--pick-head-x, 50%); top: var(--pick-head-y, 40%);
  transform: translate(-50%, 0.35rem);
  margin: 0; max-width: min(88vw, 26rem); width: max-content; padding: 0.3rem 0.7rem;
  border-radius: 999px; background: rgba(24, 22, 19, 0.72); color: #f2ecdf;
  font-size: 0.82rem; line-height: 1.3; text-align: center;
  opacity: 0; transition: opacity 0.3s ease; pointer-events: none;
}
.hero-pick-note.is-on { opacity: 1; }
.hero-intro.is-over-bird { cursor: pointer; }
.hero-pick-dot:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.hero-pick.is-locked .hero-pick-arrow, .hero-pick.is-locked .hero-pick-dots { opacity: 0; pointer-events: none; }
.hero-intro.is-playing .hero-pick-line { opacity: 0; }
/* The pill under the stump: "Be the Toucan" in the button serif, then the loading pill. */
.hero-intro.is-select .hero-play { top: var(--pick-base-y, 80%); min-width: 0; }
.hero-play[data-state="choose"] {
  pointer-events: auto; cursor: pointer; padding: 0.7rem 1.7rem 0.75rem;
  font-family: var(--btn-font); font-size: 1.15rem; letter-spacing: 0; font-weight: 600;
}
.hero-play[data-state="choose"]:hover { background: rgba(24, 22, 19, 0.86); }
.hero-play[data-state="choose"] .hero-play-track { display: none; }
.hero-play:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
/* The title before the picker: the place alone and a big "▶ Play" under the stump. The consent
   line only comes with the picker, right under "Be the …" and its dots. */
.hero-play[data-state="title"] {
  pointer-events: auto; cursor: pointer; min-width: 0; gap: 0;
  padding: 0.85rem 2.4rem 0.9rem; border-radius: 999px;
  font-family: var(--btn-font); font-size: 1.35rem; letter-spacing: 0.02em; font-weight: 600;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.3);
  transition: transform 0.15s ease, background 0.15s ease;
}
.hero-play[data-state="title"]:hover { background: rgba(24, 22, 19, 0.86); transform: translate(-50%, -50%) scale(1.04); }
.hero-play[data-state="title"] .hero-play-track { display: none; }
.hero-intro.is-select:not(.is-choosing) .hero-play-note { display: none; }
.hero-intro.is-select .hero-play-note {
  top: calc(var(--pick-base-y, 80%) + 3.6rem); bottom: auto; max-width: min(92%, 27rem);
}
.hero-intro.is-select.is-playing .hero-play-note { opacity: 0; transition: opacity 0.4s ease; }
/* The game-stats ask (hero-intro.js, hero-gamestats.js): until a visitor has answered, a small
   card under "Be the …" and its dots says the game is still in development and asks for
   anonymous game stats; it carries the play-data note's words meanwhile. An answer turns it
   into a one-line thank-you that fades. */
.hero-stats-ask {
  position: absolute; right: 1.5rem; top: 5.5rem;
  box-sizing: border-box; width: min(30vw, 21rem);
  padding: 0.8rem 1rem 0.65rem; border-radius: 0.9rem;
  background: rgba(24, 22, 19, 0.74); color: #f2ecdf;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.25);
  font-size: 0.82rem; line-height: 1.4; text-align: left; text-wrap: pretty;
  transition: opacity 0.5s ease;
}
.hero-intro.is-select:not(.is-choosing) .hero-stats-ask { display: none; }
.hero-intro.is-select.is-playing .hero-stats-ask, .hero-stats-ask.is-gone { opacity: 0; pointer-events: none; }
.hero-stats-ask.is-answered { padding: 0.55rem 1rem; }
.hero-stats-ask-text { margin: 0; }
.hero-stats-ask-text strong { color: #fff; font-weight: 700; }
.hero-stats-ask-btns { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.65rem; }
.hero-stats-ask-btn {
  padding: 0.42rem 1.1rem; border: 0; border-radius: 999px; cursor: pointer;
  font-family: var(--btn-font); font-size: 0.95rem; font-weight: 600; line-height: 1.2;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.15s ease, background 0.15s ease;
}
.hero-stats-ask-btn.is-yes { background: #f2ecdf; color: #1a1815; }
.hero-stats-ask-btn.is-yes:hover { background: #fff; transform: scale(1.04); }
.hero-stats-ask-btn.is-no { background: transparent; color: #f2ecdf; box-shadow: inset 0 0 0 1px rgba(242, 236, 223, 0.45); }
.hero-stats-ask-btn.is-no:hover { box-shadow: inset 0 0 0 1px rgba(242, 236, 223, 0.85); }
.hero-stats-ask-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.hero-stats-ask-small { margin: 0.55rem 0 0; font-size: 0.68rem; line-height: 1.35; color: rgba(242, 236, 223, 0.68); }
.hero-stats-ask-small a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.hero-play-note.is-asked { visibility: hidden; }
.hero-stats-ask-link { display: none; }
/* Phones: the corner would cover the bird — the card sits at the foot of the hero, under the
   dots, shorter (the note's line waits until it is answered; the privacy link rides along). */
@media (max-width: 640px) {
  .hero-stats-ask { left: 0.6rem; right: 0.6rem; top: auto; bottom: 1.3rem; width: auto; padding: 0.5rem 0.8rem 0.45rem; font-size: 0.76rem; line-height: 1.3; } /* over the credit line */
  .hero-stats-ask-small { display: none; }
  .hero-stats-ask-btns { align-items: center; margin-top: 0.4rem; }
  .hero-stats-ask-btn { padding: 0.36rem 0.9rem; font-size: 0.88rem; }
  .hero-stats-ask-link { display: inline; margin-left: auto; font-size: 0.7rem; color: rgba(242, 236, 223, 0.7); text-decoration: underline; text-underline-offset: 2px; }
}
/* A short phone has no room under the dots: the card takes the name's place at the top while
   the question is up (the name steps aside until it is answered or the bird is picked). */
@media (max-width: 640px) and (max-height: 760px) {
  .hero-stats-ask { top: 3.4rem; bottom: auto; }
  .hero-intro.is-choosing.has-ask:not(.is-playing) ~ .hero-copy { visibility: hidden; }
}
.hero-pick button, .hero-intro.is-select .hero-play { -webkit-tap-highlight-color: transparent; }
/* Phones: no room beside the stump — the signs flank the pill instead, at the screen's edges,
   a little smaller (still a full thumb target). */
@media (max-width: 640px) {
  .hero-pick-arrow { width: 3.7rem; height: 4rem; top: var(--pick-base-y, 80%); }
  .hero-pick-arrow.is-prev { left: 0.9rem; transform: translateY(-58%); }
  .hero-pick-arrow.is-next { left: auto; right: 0.9rem; transform: translateY(-58%); }
  .hero-pick-arrow.is-prev:hover, .hero-pick-arrow.is-next:hover { transform: translateY(-58%); }
  .hero-pick-arrow:active .hero-pick-plank { filter: brightness(1.12); }
  .hero-play[data-state="choose"] { font-size: 1.05rem; padding: 0.65rem 1.3rem 0.7rem; }
  .hero-pick-line { font-size: 1.5rem; }
}
/* Token overlays (shelf-kit projects them over the canvas): the select card (informational,
   not a link) and the one-word hover/select tag (project.heroLabel). Both translate(-50%,-100%)
   so the projected point is their bottom-center anchor. */
.table-card {
  position: absolute; z-index: 5;
  transform: translate(-50%, -100%);
  display: flex; align-items: stretch; gap: 0.6rem;
  padding: 0.55rem 0.65rem; max-width: 15rem;
  background: var(--bg); border: 1px solid var(--ink); border-radius: 0.5rem;
  box-shadow: 0 8px 22px rgba(26, 26, 26, 0.18);
  text-decoration: none; color: var(--ink);
  /* Informational overlay, not a link/document: default cursor, no text selection. */
  cursor: default; -webkit-user-select: none; user-select: none;
}
/* Text (~60%) beside a cover thumbnail (~40%) when the project has one; text-only cards
   stay compact and single-column. */
.table-card.has-media { width: 18rem; max-width: 80vw; }
.table-card-body { display: flex; flex-direction: column; gap: 0.15rem; align-items: flex-start; min-width: 0; }
.table-card.has-media .table-card-body { flex: 1 1 60%; }
.table-card strong { font-family: "Fraunces", Georgia, serif; font-size: 0.95rem; line-height: 1.2; }
.table-year { font-family: var(--mono); font-size: 0.72rem; opacity: 0.6; }
.table-blurb {
  font-size: 0.8rem; line-height: 1.35; margin-top: 0.1rem;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden;
}
.table-card-media { flex: 0 0 40%; display: flex; }
.table-card-media img {
  width: 100%; height: 100%; max-height: 6rem; min-height: 3.5rem;
  object-fit: cover; border-radius: 0.35rem; display: block;
  /* Inert decoration: no drag, no selection, no link cursor. A click on it is caught by
     the card (it stays open); a right-click is cancelled by the card's contextmenu guard.
     NOT pointer-events:none — that would let the click fall through to the token behind. */
  -webkit-user-drag: none; user-select: none; cursor: default;
}
.shelf-tag {
  position: absolute; z-index: 4;
  transform: translate(-50%, -100%);
  padding: 0.2rem 0.6rem;
  background: var(--ink); color: var(--bg);
  border-radius: 999px;
  font-family: var(--mono); font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase;
  white-space: nowrap; pointer-events: none;
}

/* Name + subline overlay: DOM above the filtered canvas, so type stays crisp.
   pointer-events:none lets cube drags pass through the text; controls opt back in. */
.hero-copy {
  position: absolute; top: 6.2rem; left: 0; right: 0; z-index: 5; /* above the loading cover +
     progress pill (z3/z4) so the name reads from the very first paint; still below the
     project-card overlay (z6) and the floating nav (z99+) */
  text-align: center; pointer-events: none;
  /* Asymmetric fade: the name creeps BACK over ~20s once the visitor idles (an instant
     return reads as the UI barging in mid-pause), but still clears out fast on play. */
  transition: opacity 20s ease;
}
.hero-copy--faded { opacity: 0; transition: opacity 2s ease; }
/* The hat picker is open: the name block sits above the game's layer and would show through
   its cards — it steps aside until the choice is made (hero-game). */
.hero-copy--picking { visibility: hidden; }
.hero-copy a, .hero-copy button { pointer-events: auto; }
.hero-name {
  margin: 0;
  font-family: "Fraunces", Georgia, serif; font-weight: 900;
  font-size: clamp(2.6rem, 7vw, 5rem); letter-spacing: -0.015em; line-height: 1;
  color: #fff; text-shadow: 0 2px 24px rgba(31, 63, 110, 0.35);
}
.hero-name::after { display: none; } /* opt out of the page-title accent bar */
.hero-subline {
  margin: 0.6rem 0 0;
  font-size: 1.05rem; color: rgba(255, 255, 255, 0.92);
  text-shadow: 0 1px 12px rgba(31, 63, 110, 0.4);
}
.hero-location {
  margin: 0.8rem 0 0;
  font-family: var(--mono); font-size: 0.72rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.14em;
  color: rgba(255, 255, 255, 0.85);
  text-shadow: 0 1px 12px rgba(31, 63, 110, 0.4);
}
/* The live presence line under the name (hero-presence.js): "3 other birds here right now"
   with a live dot, or — alone — a quieter "other visitors show up here as birds". Hidden
   until a presence server answered (never on a static host). It fades with the name while
   the visitor plays; the top-right chip (.hero-online) carries the count then — never both. */
.hero-presence {
  display: flex; align-items: center; gap: 0.45rem;
  width: fit-content; max-width: calc(100% - 32px);
  margin: 0.85rem auto 0; padding: 0.26rem 0.7rem 0.3rem;
  background: rgba(22, 20, 17, 0.34); border-radius: 6px;
  font: 600 0.8rem/1.2 "Overused Grotesk", system-ui, sans-serif;
  color: #fff; text-shadow: 0 1px 8px rgba(20, 35, 60, 0.45);
  animation: hero-presence-in 0.5s ease backwards;
}
.hero-presence[hidden] { display: none; }
.hero-presence::before {
  content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%;
  background: #7ed07e; box-shadow: 0 0 6px rgba(126, 208, 126, 0.8);
}
.hero-presence.is-alone { color: rgba(255, 255, 255, 0.86); font-weight: 500; }
.hero-presence.is-alone::before { background: rgba(255, 255, 255, 0.6); box-shadow: none; }
/* A notice borrowing the line before the visitor plays ("Pigeon landed in the undercroft."). */
.hero-presence.is-notice::before { background: #ffd76e; box-shadow: 0 0 6px rgba(255, 215, 110, 0.8); }
@keyframes hero-presence-in { from { opacity: 0; transform: translateY(4px); } }
.hero:has(.hero-copy:not(.hero-copy--faded) .hero-presence:not([hidden])) .hero-online { display: none; }
/* The small credit line along the hero's bottom edge (home-sections.js heroSection): work in
   progress · open to feedback! · Co-built with Claude. Below every HUD piece (they stop at
   bottom: 12px), quiet until hovered. */
.hero-credit {
  position: absolute; left: 50%; bottom: 1px; z-index: 5; /* level with the title (.hero-intro), after it in the DOM */
  transform: translateX(-50%);
  margin: 0; white-space: nowrap;
  font: 500 0.62rem/1.2 "Overused Grotesk", system-ui, sans-serif; letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.6); text-shadow: 0 1px 4px rgba(20, 30, 40, 0.6);
  opacity: 0.75; transition: opacity 0.3s ease;
}
.hero-credit:hover, .hero-credit:focus-within { opacity: 1; }
.hero-credit a { color: inherit; text-decoration: underline; text-decoration-color: rgba(255, 255, 255, 0.35); text-underline-offset: 2px; }
.hero-credit a:hover { color: #fff; }
/* ⚙ chip (hero-scene.js): the player's settings — screen shake and quality. It comes in once
   the visitor has played, in the ♪ chip's column: just above it, or in its place before the ♪
   arrives. The touch layouts (below) put it under the ♪ instead. */
.hero-gear {
  position: absolute; right: 10px; bottom: 52px; z-index: 3;
  width: 34px; height: 34px; border-radius: 50%;
  font-size: 17px; line-height: 1; padding: 0;
  background: rgba(24, 22, 19, 0.55); color: #e8e2d6;
  border: 1px solid rgba(255, 255, 255, 0.2); cursor: pointer;
  animation: hero-sound-chip-in 0.6s ease both;
}
.hero-gear[hidden] { display: none; }
.hero-scene:not(:has(> .hero-sound-chip)) > .hero-gear { bottom: 10px; }
.hero-gear:hover, .hero-gear[aria-expanded="true"] { background: rgba(24, 22, 19, 0.85); }
/* ♪ music/ambience toggle (hero-soundtrack.js) — appears once the beds arm, bottom right in
   the column the dock leaves free. */
.hero-sound-chip {
  position: absolute; right: 10px; bottom: 10px; z-index: 3;
  width: 34px; height: 34px; border-radius: 50%;
  font: 700 17px/1 var(--hud-emoji, sans-serif); padding: 0;
  background: rgba(24, 22, 19, 0.55); color: #ffd76e;
  border: 1px solid rgba(255, 255, 255, 0.2); cursor: pointer;
  animation: hero-sound-chip-in 0.6s ease both;
}
.hero-sound-chip:hover { background: rgba(24, 22, 19, 0.85); }
.hero-sound-chip.is-muted { color: #e8e2d6; font-size: 15px; }
@keyframes hero-sound-chip-in { from { opacity: 0; transform: scale(0.6); } to { opacity: 1; transform: none; } }
/* The settings card the ⚙ opens: over the middle of the meadow, the game running behind it. */
.hero-settings {
  position: absolute; left: 50%; top: 50%; z-index: 7;
  transform: translate(-50%, -50%);
  width: min(19rem, calc(100% - 32px)); max-height: calc(100% - 24px); overflow: auto;
  padding: 0.9rem 1rem 0.8rem; border-radius: 12px;
  background: rgba(24, 22, 19, 0.9); color: #f3eee4;
  border: 1px solid rgba(255, 255, 255, 0.16); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
  font: 0.86rem/1.35 var(--mono);
}
.hero-settings[hidden] { display: none; }
.hero-settings-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem; }
.hero-settings-head h3 { margin: 0; font: 600 1.15rem/1.2 var(--btn-font); color: #fff; }
.hero-settings-close {
  position: relative; width: 28px; height: 28px; border-radius: 50%; padding: 0;
  font: 14px/1 var(--mono); color: #e8e2d6; background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.16); cursor: pointer;
}
.hero-settings-close:hover { background: rgba(255, 255, 255, 0.18); }
.hero-settings-close::after { content: ""; position: absolute; inset: -8px; } /* a 44 px target */
/* Fingers: the rows, the quality segments and the shake slider reach 44 px tall. */
@media (pointer: coarse) {
  .hero-settings-row { min-height: 44px; }
  .hero-settings-row input[type="range"] { min-height: 44px; }
  .hero-settings-seg span { padding: 0.72rem 0.8rem; }
}
.hero-settings-row { display: flex; align-items: center; gap: 0.6rem; min-height: 2.1rem; }
.hero-settings-label { flex: 1 1 auto; }
.hero-settings-label small { display: block; font-size: 0.72rem; color: rgba(243, 238, 228, 0.6); }
.hero-settings-row input[type="range"] { flex: 0 1 7rem; min-width: 0; accent-color: #ffd76e; }
.hero-settings-row > .hero-settings-label { white-space: nowrap; }
.hero-settings-val { flex: 0 0 2.6rem; text-align: right; font-variant-numeric: tabular-nums; color: #ffd76e; }
.hero-settings-sub {
  margin: 0.55rem 0 0.1rem; padding-top: 0.55rem; border-top: 1px solid rgba(255, 255, 255, 0.12);
  font-size: 0.66rem; font-weight: 800; letter-spacing: 0.16em; text-transform: uppercase; color: #ffd76e;
}
.hero-settings-switch { cursor: pointer; }
.hero-settings-switch input {
  appearance: none; -webkit-appearance: none; flex: 0 0 auto; margin: 0;
  position: relative; width: 38px; height: 22px; border-radius: 11px; cursor: pointer;
  background: rgba(255, 255, 255, 0.2); transition: background 0.15s;
}
.hero-settings-switch input::after {
  content: ""; position: absolute; left: 3px; top: 3px; width: 16px; height: 16px; border-radius: 50%;
  background: #f3eee4; transition: transform 0.15s;
}
.hero-settings-switch input:checked { background: #c98a4b; }
.hero-settings-switch input:checked::after { transform: translateX(16px); }
.hero-settings-switch input:disabled { opacity: 0.4; cursor: default; }
.hero-settings-switch:has(input:disabled) { cursor: default; }
.hero-settings-switch input:focus-visible, .hero-settings-seg input:focus-visible + span,
.hero-settings-row input[type="range"]:focus-visible { outline: 2px solid #ffd76e; outline-offset: 2px; }
.hero-settings-seg { display: flex; border-radius: 8px; overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.2); }
.hero-settings-seg label { position: relative; cursor: pointer; }
.hero-settings-seg input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.hero-settings-seg span { display: block; padding: 0.28rem 0.55rem; font-size: 0.78rem; }
.hero-settings-seg label + label span { border-left: 1px solid rgba(255, 255, 255, 0.2); }
.hero-settings-seg input:checked + span { background: #c98a4b; color: #fff; }
.hero-settings-note { margin: 0.6rem 0 0; font-size: 0.7rem; color: rgba(243, 238, 228, 0.55); }

/* Meadow multiplayer overlays (hero-avatar.js / hero-presence.js). Emotes and name tags
   are projected DOM anchored bottom-center on a world point, exactly like .shelf-tag.
   The chat (the 3 phrase buttons on keys 1–3 + the log) lives bottom-center — see .hero-chatbox. */
/* The rise-and-dissolve text (hero-game's collect / level-up toasts): positioned once at
   spawn, then the animation carries it up and out. (Phrases a bird says use .hero-said.) */
.hero-emote {
  position: absolute; z-index: 5;
  transform: translate(-50%, -100%);
  font-family: "Fraunces", Georgia, serif; font-weight: 700;
  font-size: 1.05rem; color: #fff;
  text-shadow: 0 2px 14px rgba(31, 63, 110, 0.5), 0 0 2px rgba(31, 63, 110, 0.35);
  white-space: nowrap; pointer-events: none;
  animation: hero-emote-rise 2.6s ease-out forwards;
}
@keyframes hero-emote-rise {
  0% { opacity: 0; margin-top: 8px; }
  12% { opacity: 1; }
  70% { opacity: 0.9; }
  100% { opacity: 0; margin-top: -90px; }
}
.hero-nametag {
  position: absolute; z-index: 4;
  transform: translate(-50%, -100%);
  padding: 0.12rem 0.45rem 0.16rem;
  background: rgba(22, 20, 17, 0.62); color: var(--hud-ink);
  border-radius: 4px;
  font-family: "Overused Grotesk", system-ui, sans-serif; font-size: 0.72rem; font-weight: 700;
  white-space: nowrap; pointer-events: none;
  text-align: center;
}
/* Second nametag line: species + level ("Crow — Lvl. 3") — the veteran's visible mark. */
.hero-nametag-sub { display: block; margin-top: 1px; color: var(--hud-dim); font-size: 0.62rem; font-weight: 600; }
.hero-nametag-sub:empty { display: none; }
/* Shared HUD tokens (hero-hud.js / hero-chat.js / presence overlays): one dark glass, one
   hairline, one gold. Corners are small radii — rectangles and circles only, no pills. The
   type pairs like the rest of the site: Fraunces for names and numbers, Overused Grotesk
   (sentence case) for everything else; tiny tracked capitals only for group captions. */
:root {
  --hud-bg: rgba(22, 20, 17, 0.74);
  --hud-bg-strong: rgba(22, 20, 17, 0.86);
  --hud-line: rgba(255, 255, 255, 0.16);
  --hud-ink: #f2ecdf;
  --hud-dim: rgba(242, 236, 223, 0.62);
  --hud-gold: #ffd76e;
  --hud-radius: 8px;
  --hud-shadow: 0 6px 18px rgba(12, 22, 38, 0.22);
  --hud-text-shadow: 0 1px 8px rgba(20, 35, 60, 0.6);
  --hud-emoji: "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", sans-serif;
}
.hero-key {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  min-width: 1.25rem; height: 1.25rem; padding: 0 0.3rem;
  font: 700 0.64rem/1 "Overused Grotesk", system-ui, sans-serif; color: var(--hud-ink);
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.24); border-bottom-width: 2px; border-radius: 4px;
}
/* Chat, bottom-center above the level bar (hero-chat.js): the say row — three phrase
   buttons on keys 1–3 (.is-special = a nearby object's gold phrase; .is-fresh pops when a
   wording re-rolls) — and above it the lines: fresh ones on their own small dark plates,
   fading out after a while (three at most, so the bird — the camera keeps it a little
   above this band — stays in view). The level bar sits bottom-left, the dock
   bottom-right. */
.hero-chatbox {
  position: absolute; left: 50%; bottom: 12px; z-index: 3;
  transform: translateX(-50%);
  width: min(640px, calc(100% - 2 * (min(300px, 23vw) + 24px)));
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  font-family: "Overused Grotesk", system-ui, sans-serif;
  font-size: 0.8rem; line-height: 1.35; color: var(--hud-ink);
  pointer-events: none;
}
.hero-chatbox[hidden] { display: none; }
/* Thoreau's Flute (hero-flute.js): while it's raised, its eight notes take the phrase row's
   place — keys 1–8, each note its own hue — and the ♪ float up off whoever plays. */
.hero-flute {
  position: absolute; left: 50%; bottom: 12px; z-index: 4;
  transform: translateX(-50%);
  display: none; flex-direction: column; align-items: center; gap: 6px;
  font-family: "Overused Grotesk", system-ui, sans-serif; color: var(--hud-ink);
  pointer-events: none;
}
.hero-scene[data-flute-up] .hero-flute { display: flex; }
.hero-scene[data-flute-up] .hero-say-row { visibility: hidden; }
.hero-flute-hint { font-size: 0.74rem; color: var(--hud-dim); text-shadow: var(--hud-text-shadow); }
.hero-flute-keys { display: flex; gap: 5px; pointer-events: auto; }
.hero-flute-key {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  width: 2.7rem; padding: 0.4rem 0 0.45rem;
  font: 600 1rem/1 "Fraunces", Georgia, serif; color: var(--hud-ink);
  background: var(--hud-bg);
  border: 1px solid var(--hud-line);
  border-bottom: 3px solid hsl(var(--note-hue, 40), 70%, 62%);
  border-radius: var(--hud-radius);
  box-shadow: var(--hud-shadow);
  cursor: pointer; user-select: none; touch-action: none;
  transition: transform 0.08s ease, background-color 0.12s ease, color 0.12s ease;
}
.hero-flute-key:hover { background: var(--hud-bg-strong); }
.hero-flute-key .hero-key { font-size: 0.66rem; }
.hero-flute-key.is-on {
  transform: translateY(2px);
  color: var(--hud-gold);
  background: linear-gradient(180deg, rgba(86, 64, 22, 0.94), rgba(46, 35, 14, 0.94));
  border-color: rgba(255, 215, 110, 0.8);
  border-bottom-color: hsl(var(--note-hue, 40), 85%, 68%);
}
.hero-flute-note {
  position: absolute; z-index: 3; pointer-events: none;
  font: 700 1.25rem/1 "Fraunces", Georgia, serif;
  color: hsl(var(--note-hue, 40), 85%, 78%);
  text-shadow: 0 1px 6px rgba(20, 35, 60, 0.55);
  transform: translate(-50%, -50%);
  animation: hero-flute-note 1.4s ease-out forwards;
}
.hero-flute-note.is-mine { font-size: 1.4rem; }
@keyframes hero-flute-note {
  0% { opacity: 0; transform: translate(-50%, -50%) scale(0.6); }
  15% { opacity: 1; transform: translate(-50%, -80%) scale(1); }
  100% { opacity: 0; transform: translate(calc(-50% + var(--drift, 0px)), -320%) scale(1.1) rotate(-12deg); }
}
/* The streak (hero-flute showStreak): one gold pip per right note of a song's opening and a dim
   one pulsing after them; the key that made it flares gold, its ♪ floats up gold and bigger the
   further in. A broken melody shakes the pips and drops them. */
.hero-flute-streak { display: flex; align-items: center; justify-content: center; gap: 7px; height: 16px; }
.hero-flute-pip {
  width: 14px; height: 14px; border-radius: 50%;
  background: rgba(255, 255, 255, 0.14); border: 1px solid rgba(255, 255, 255, 0.28);
}
.hero-flute-pip.is-lit {
  background: radial-gradient(circle at 35% 35%, #fff6cc, var(--hud-gold) 55%, #e0a020);
  border-color: rgba(255, 236, 170, 0.9);
  box-shadow: 0 0 10px rgba(255, 210, 90, 0.9);
}
.hero-flute-pip.is-new { animation: hero-flute-pip-in 0.45s cubic-bezier(0.3, 1.6, 0.5, 1); }
.hero-flute-pip.is-next { animation: hero-flute-pip-next 1s ease-in-out infinite; }
.hero-flute-streak.is-broken .hero-flute-pip { animation: hero-flute-pip-drop 0.42s ease-in forwards; }
@keyframes hero-flute-pip-in {
  0% { transform: scale(0.2); box-shadow: 0 0 0 rgba(255, 210, 90, 0); }
  55% { transform: scale(1.7); box-shadow: 0 0 16px rgba(255, 220, 110, 1); }
  100% { transform: scale(1); }
}
@keyframes hero-flute-pip-next {
  0%, 100% { opacity: 0.35; transform: scale(0.85); }
  50% { opacity: 1; transform: scale(1.05); }
}
@keyframes hero-flute-pip-drop {
  0% { transform: none; }
  25% { transform: translateX(-3px); }
  50% { transform: translateX(3px); opacity: 1; }
  100% { transform: translateY(10px) scale(0.6); opacity: 0; }
}
.hero-flute.is-streak .hero-flute-hint { color: var(--hud-gold); font-weight: 700; }
.hero-flute-key.is-in-streak {
  color: var(--hud-gold);
  border-color: rgba(255, 215, 110, 0.85);
  box-shadow: 0 0 12px 2px rgba(255, 210, 90, 0.55), var(--hud-shadow);
}
.hero-flute-key.is-streak { animation: hero-flute-key-streak 0.6s ease-out; }
@keyframes hero-flute-key-streak {
  0% { box-shadow: 0 0 0 0 rgba(255, 214, 102, 0.95), var(--hud-shadow); }
  40% { box-shadow: 0 0 18px 5px rgba(255, 214, 102, 0.8), var(--hud-shadow); }
  100% { box-shadow: 0 0 0 0 rgba(255, 214, 102, 0), var(--hud-shadow); }
}
.hero-flute-note.is-streak {
  color: #ffe08a;
  font-size: calc(1.4rem + var(--streak, 1) * 0.14rem);
  text-shadow: 0 0 10px rgba(255, 210, 90, 0.95), 0 1px 6px rgba(20, 35, 60, 0.55);
}
/* A song went off (hero-flute celebrate): every key flares gold in a wave from C up, the bar
   folds away, the ♪ still in the air burst, and the song's name rises over the screen in the
   hand lettering with sparks flying off it. */
.hero-flute.is-cast { animation: hero-flute-fold 1.1s ease-in forwards; }
.hero-flute.is-cast .hero-flute-key {
  animation: hero-flute-key-cast 0.7s ease-out both;
  animation-delay: calc(var(--note-hue, 0) * 1.2ms);
}
.hero-flute.is-cast .hero-flute-streak { visibility: hidden; }
@keyframes hero-flute-fold {
  0%, 55% { opacity: 1; translate: 0 0; }
  100% { opacity: 0; translate: 0 22px; }
}
@keyframes hero-flute-key-cast {
  0% { transform: none; }
  35% { transform: translateY(-9px) scale(1.12); color: #fff3c0; border-color: rgba(255, 225, 130, 1);
    background: linear-gradient(180deg, rgba(120, 90, 25, 0.96), rgba(70, 52, 18, 0.96));
    box-shadow: 0 0 22px 6px rgba(255, 214, 102, 0.85), var(--hud-shadow); }
  100% { transform: none; }
}
.hero-flute-note.is-spent { animation: hero-flute-note 1.4s ease-out forwards, hero-flute-note-spent 0.5s ease-out forwards; }
@keyframes hero-flute-note-spent {
  0% { scale: 1; opacity: 1; }
  40% { scale: 2.2; opacity: 1; color: #fff3c0; text-shadow: 0 0 16px rgba(255, 220, 110, 1); }
  100% { scale: 3; opacity: 0; }
}
.hero-flute-song {
  position: absolute; left: 50%; top: 32%; z-index: 6;
  transform: translate(-50%, -50%);
  pointer-events: none;
  animation: hero-flute-song 2.6s ease-out forwards;
}
.hero-flute-song-name {
  --ink: rgba(60, 38, 8, 0.9);
  display: block; white-space: nowrap;
  font: 700 3.4rem/1 "Caveat", "Segoe Print", "Comic Sans MS", cursive; color: #ffe08a;
  text-shadow:
    -2px 0 0 var(--ink), 2px 0 0 var(--ink), 0 -2px 0 var(--ink), 0 2px 0 var(--ink),
    0 0 22px rgba(255, 210, 90, 0.9), 0 4px 12px rgba(10, 20, 10, 0.4);
}
.hero-flute-song-spark {
  position: absolute; left: 50%; top: 50%;
  font: 700 1.5rem/1 "Fraunces", Georgia, serif;
  color: hsl(var(--note-hue, 40), 90%, 78%);
  text-shadow: 0 0 10px rgba(255, 230, 150, 0.9);
  animation: hero-flute-song-spark 1.5s ease-out forwards;
}
@keyframes hero-flute-song {
  0% { opacity: 0; transform: translate(-50%, -30%) scale(0.6); }
  14% { opacity: 1; transform: translate(-50%, -50%) scale(1.08); }
  24% { transform: translate(-50%, -50%) scale(1); }
  75% { opacity: 1; transform: translate(-50%, -62%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -80%) scale(1.04); }
}
@keyframes hero-flute-song-spark {
  0% { opacity: 0; transform: translate(-50%, -50%) rotate(var(--a, 0deg)) translateY(0) scale(0.4); }
  20% { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -50%) rotate(var(--a, 0deg)) translateY(calc(var(--d, 90px) * -1)) scale(1.1); }
}
.hero-flute.is-narrow .hero-flute-keys { display: grid; grid-template-columns: repeat(4, 1fr); }
@media (max-width: 520px) {
  .hero-flute-keys { display: grid; grid-template-columns: repeat(4, 1fr); }
  .hero-flute-key { width: 3rem; padding: 0.55rem 0; }
  .hero-flute-song-name { font-size: 2.4rem; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-flute-note { animation-duration: 0.01s; }
  .hero-flute-pip, .hero-flute-key.is-streak, .hero-flute.is-cast .hero-flute-key, .hero-flute-song-spark { animation: none; }
  .hero-flute-song { animation-name: hero-flute-song-still; }
}
@keyframes hero-flute-song-still {
  0%, 100% { opacity: 0; }
  8%, 80% { opacity: 1; }
}
.hero-chat-lines {
  display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: 3px;
  width: 100%; max-height: 5.2rem; overflow: hidden;
}
.hero-chat-line {
  max-width: 100%; padding: 0.2rem 0.6rem;
  background: rgba(22, 20, 17, 0.64); border-radius: 4px;
  overflow-wrap: anywhere;
  transition: opacity 1.2s ease;
  animation: hero-chat-in 0.25s ease-out;
}
@keyframes hero-chat-in {
  from { opacity: 0; transform: translateY(6px); }
}
.hero-chat-line.is-fading { opacity: 0; }
.hero-chat-who { font-weight: 700; margin-right: 0.45em; color: var(--hud-gold); }
.hero-chat-line--self .hero-chat-who { color: #a8dca0; }
.hero-chat-line--sys { font-size: 0.74rem; color: var(--hud-dim); background: rgba(22, 20, 17, 0.5); }
.hero-chat-line.is-special .hero-chat-text { color: var(--hud-gold); font-weight: 700; }
.hero-say-row {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  max-width: 100%;
}
.hero-says { display: flex; gap: 6px; min-width: 0; }
.hero-say {
  display: inline-flex; align-items: center; gap: 0.45rem; min-width: 0; max-width: 13.5rem;
  padding: 0.36rem 0.75rem 0.36rem 0.36rem;
  font: 600 0.84rem/1.2 "Overused Grotesk", system-ui, sans-serif; color: var(--hud-ink);
  background: var(--hud-bg);
  border: 1px solid var(--hud-line); border-radius: var(--hud-radius);
  box-shadow: var(--hud-shadow);
  cursor: pointer; white-space: nowrap; pointer-events: auto; user-select: none;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}
.hero-say:hover { background: var(--hud-bg-strong); border-color: rgba(255, 255, 255, 0.34); }
.hero-say-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.hero-say.is-fresh { animation: hero-say-fresh 0.35s cubic-bezier(0.2, 1.5, 0.4, 1); }
@keyframes hero-say-fresh {
  0% { transform: scale(0.9); }
  60% { transform: scale(1.04); }
  100% { transform: scale(1); }
}
/* The special: gold words on a warm plate, a star before them, a slow glow. */
.hero-say.is-special {
  color: var(--hud-gold);
  background: linear-gradient(180deg, rgba(86, 64, 22, 0.94), rgba(46, 35, 14, 0.94));
  border-color: rgba(255, 215, 110, 0.8);
  animation: hero-say-glow 2.4s ease-in-out infinite;
}
.hero-say.is-special.is-fresh { animation: hero-say-fresh 0.35s cubic-bezier(0.2, 1.5, 0.4, 1), hero-say-glow 2.4s ease-in-out 0.35s infinite; }
.hero-say.is-special .hero-say-text::before { content: "\2726\00a0"; }
.hero-say.is-special .hero-key { color: var(--hud-gold); border-color: rgba(255, 215, 110, 0.6); background: rgba(255, 215, 110, 0.12); }
@keyframes hero-say-glow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 215, 110, 0.18), var(--hud-shadow); }
  50% { box-shadow: 0 0 16px 2px rgba(255, 215, 110, 0.45), var(--hud-shadow); }
}
/* Said lines (hero-presence.js): what a bird just said, hand-lettered over its head for a
   few seconds (no bubble) — camera-tracked, so the words stay with the speaker. Caveat,
   white with a dark pen outline so it reads on grass and sky; the letters spill out one by
   one (--i), each with its own tilt and baseline (--r / --y). Loud lines (.fx-*, from
   presence-phrases.js LOOKS) colour each letter (--c), keep moving (.mo-*: the inner span,
   so the spill-out and the motion don't fight over transform) and draw squiggles off the
   words (.hero-said-lines). A peer's line clears its two-line nametag (--lift). */
.hero-said {
  --lift: 40px;
  --ink: rgba(24, 32, 20, 0.88);
  position: absolute; z-index: 5;
  transform: translate(-50%, calc(-100% - var(--lift)));
  width: max-content; max-width: 16rem;
  font: 700 1.7rem/1.05 "Caveat", "Segoe Print", "Comic Sans MS", cursive; color: #fff;
  text-align: center; text-wrap: balance;
  text-shadow:
    -1.5px 0 0 var(--ink), 1.5px 0 0 var(--ink), 0 -1.5px 0 var(--ink), 0 1.5px 0 var(--ink),
    -1px -1px 0 var(--ink), 1px -1px 0 var(--ink), -1px 1px 0 var(--ink), 1px 1px 0 var(--ink),
    0 3px 8px rgba(10, 20, 10, 0.35);
  pointer-events: none;
  opacity: 0; transition: opacity 0.45s ease;
}
.hero-said.is-self { --lift: 8px; }
.hero-said.is-in { opacity: 1; }
.hero-said:not(.fx-plain) { font-size: 1.95rem; }
.hero-said-text { position: relative; display: inline; }
.hero-said-w { white-space: nowrap; display: inline-block; }
.hero-said-c {
  display: inline-block;
  color: var(--c, #fff);
  transform: translateY(var(--y)) rotate(var(--r));
}
.hero-said-c > span { display: inline-block; }
.hero-said.is-in .hero-said-c {
  animation: hero-said-in 0.32s cubic-bezier(0.2, 1.7, 0.4, 1) both;
  animation-delay: calc(var(--i) * 32ms);
}
.hero-said.fx-plain.is-in .hero-said-c { animation-duration: 0.22s; animation-delay: calc(var(--i) * 22ms); }
@keyframes hero-said-in {
  from { opacity: 0; transform: translateY(calc(var(--y) + 10px)) rotate(calc(var(--r) * -2)) scale(0.3); }
}
/* The motions loud letters keep doing, staggered along the line. */
.hero-said.mo-wave .hero-said-c > span { animation: hero-said-wave 1.1s ease-in-out infinite; animation-delay: calc(var(--i) * 90ms); }
.hero-said.mo-hop .hero-said-c > span { animation: hero-said-hop 0.8s cubic-bezier(0.3, 0, 0.6, 1) infinite; animation-delay: calc(var(--i) * 65ms); }
.hero-said.mo-wobble .hero-said-c > span { animation: hero-said-wobble 0.55s ease-in-out infinite alternate; animation-delay: calc(var(--i) * -110ms); }
.hero-said.mo-shake .hero-said-c > span { animation: hero-said-shake 0.24s steps(2, jump-none) infinite; animation-delay: calc(var(--i) * -70ms); }
.hero-said.mo-swell .hero-said-c > span { animation: hero-said-swell 1.5s ease-in-out infinite; animation-delay: calc(var(--i) * 110ms); }
.hero-said.mo-float .hero-said-c > span { animation: hero-said-float 2.2s ease-in-out infinite; animation-delay: calc(var(--i) * -170ms); }
.hero-said.mo-jolt .hero-said-c > span { animation: hero-said-jolt 0.9s cubic-bezier(0.2, 2.2, 0.4, 1) infinite; animation-delay: calc(var(--i) * 25ms); }
@keyframes hero-said-wave { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
@keyframes hero-said-hop { 0%, 55%, 100% { transform: translateY(0) scaleY(1); } 25% { transform: translateY(-7px) scaleY(1.06); } 60% { transform: translateY(0) scaleY(0.9); } }
@keyframes hero-said-wobble { from { transform: rotate(-11deg); } to { transform: rotate(11deg); } }
@keyframes hero-said-shake { 0% { transform: translate(-1.2px, 0.6px); } 50% { transform: translate(1.2px, -0.8px); } 100% { transform: translate(-0.6px, -0.4px); } }
@keyframes hero-said-swell { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.2); } }
/* float: a ghostly drift that fades a little (the dark, a breeze); jolt: a punch outward and
   back, all letters nearly at once (a smash, an engine). */
@keyframes hero-said-float { 0%, 100% { transform: translate(0, 0) rotate(0); opacity: 1; } 33% { transform: translate(1.5px, -4px) rotate(-4deg); opacity: 0.8; } 66% { transform: translate(-1.5px, -2px) rotate(3deg); opacity: 0.92; } }
@keyframes hero-said-jolt { 0%, 30%, 100% { transform: scale(1) translateY(0); } 8% { transform: scale(1.32) translateY(-3px); } 18% { transform: scale(0.94) translateY(1px); } }
/* Squiggles: stroked in the look's colour with the same pen outline under it (a
   drop-shadow, since SVG strokes don't take text-shadow); they draw themselves on
   (pathLength 1), then keep wiggling. */
.hero-said-lines {
  position: absolute; top: 50%; width: 2.1rem; height: 2.1rem; margin-top: -1.05rem;
  overflow: visible;
  fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;
  filter: drop-shadow(0 0 1.2px var(--ink)) drop-shadow(0 0 0.6px var(--ink));
}
.hero-said-lines.is-l { right: 100%; margin-right: 0.15rem; }
.hero-said-lines.is-r { left: 100%; margin-left: 0.15rem; transform: scaleX(-1); }
.hero-said-lines.is-t { bottom: 100%; top: auto; left: 50%; margin: 0 0 -0.3rem -1.05rem; }
.hero-said-lines path { stroke-dasharray: 1; stroke-dashoffset: 1; }
.hero-said.is-in .hero-said-lines path {
  animation: hero-said-draw 0.4s ease-out both;
  animation-delay: calc(0.12s + var(--n) * 90ms);
}
.hero-said.is-in .hero-said-lines { animation: hero-said-jiggle 0.9s ease-in-out 0.5s infinite alternate; }
.hero-said.is-in .hero-said-lines.is-r { animation-name: hero-said-jiggle-r; }
@keyframes hero-said-draw { to { stroke-dashoffset: 0; } }
@keyframes hero-said-jiggle { from { transform: rotate(-6deg) scale(0.96); } to { transform: rotate(6deg) scale(1.06); } }
@keyframes hero-said-jiggle-r { from { transform: scaleX(-1) rotate(-6deg) scale(0.96); } to { transform: scaleX(-1) rotate(6deg) scale(1.06); } }
/* Gold (a special object's phrase) glows a little, like the gold phrase button — and so do
   the context lines that want to be noticed, each in its own light: a sparkle's gold, a
   reunion's pink, a smash's fire, the undercroft's ghostly green. */
.hero-said.fx-gold, .hero-said.fx-shiny, .hero-said.fx-warm, .hero-said.fx-boom, .hero-said.fx-spooky {
  text-shadow:
    -1.5px 0 0 var(--ink), 1.5px 0 0 var(--ink), 0 -1.5px 0 var(--ink), 0 1.5px 0 var(--ink),
    -1px -1px 0 var(--ink), 1px -1px 0 var(--ink), -1px 1px 0 var(--ink), 1px 1px 0 var(--ink),
    0 0 14px var(--glow, rgba(255, 205, 80, 0.75));
}
.hero-said.fx-warm { --glow: rgba(255, 140, 180, 0.7); }
.hero-said.fx-boom { --glow: rgba(255, 120, 30, 0.8); }
.hero-said.fx-spooky { --glow: rgba(150, 255, 190, 0.6); }
/* The undercroft's straw cast (hero-straw.js, gameplay.md §49): what an owl, a cuckoo or the
   Strawman says, hand-lettered like a bird's line but in straw — pale straw letters with a brown
   pen, so a straw voice never looks like a player. A warning (an owl's judgement, the Strawman's
   shout: its attack's tell) gets a red pen and a warm glow. */
.hero-said.fx-straw {
  --ink: rgba(70, 40, 10, 0.92);
  --lift: 6px;
  color: #f6e3a4;
  font-size: 1.6rem;
  max-width: 14rem;
}
.hero-said.fx-straw.is-warn {
  --ink: rgba(120, 22, 12, 0.95);
  color: #ffeec2;
  text-shadow:
    -1.5px 0 0 var(--ink), 1.5px 0 0 var(--ink), 0 -1.5px 0 var(--ink), 0 1.5px 0 var(--ink),
    -1px -1px 0 var(--ink), 1px -1px 0 var(--ink), -1px 1px 0 var(--ink), 1px 1px 0 var(--ink),
    0 0 12px rgba(255, 120, 60, 0.6);
}
/* A cuckoo's nameplate imitates a player's ("Crow? · Lvl 2") — a shade off: straw-coloured. */
.hero-nametag.hero-nametag--fake { background: rgba(92, 66, 22, 0.66); color: #f3e2b0; }
/* A cage's tag: who sits in it, and how long (ours: the countdown and how to wriggle). */
.hero-nametag.hero-cage-tag { background: rgba(40, 24, 18, 0.72); color: #ffe7c2; }
.hero-nametag.hero-cage-tag.is-self { background: rgba(120, 32, 18, 0.8); color: #fff3dc; font-size: 0.8rem; }
@media (prefers-reduced-motion: reduce) {
  .hero-said.is-in .hero-said-c, .hero-said .hero-said-c > span, .hero-said.is-in .hero-said-lines { animation: none; }
  .hero-said.is-in .hero-said-lines path { animation: none; stroke-dashoffset: 0; }
}
/* Context text (hero-context-text.js): a line written over an object — "This gate is
   locked." — in the site's serif voice, faded in by distance per frame (inline opacity). */
.hero-ctx-text {
  position: absolute; z-index: 4;
  transform: translate(-50%, -100%);
  width: max-content; max-width: 17rem;
  font: 600 1.12rem/1.25 "Fraunces", Georgia, serif; color: #fffbef;
  letter-spacing: 0.005em; text-align: center; text-wrap: balance;
  text-shadow: 0 1px 2px rgba(18, 22, 30, 0.6), 0 0 12px rgba(20, 35, 60, 0.55), 0 2px 20px rgba(20, 35, 60, 0.35);
  pointer-events: none;
}
/* Signs (hero-sign.js): a board in the world you can read. Its title is painted onto the
   board (.hero-sign-label, mapped onto the board face every frame and multiplied onto the
   wood, so it reads as ink); a click opens the paper card (.is-open) with the whole text
   over the sign, and walking on folds it away. Near a sign with text a small chip says
   "click to read". Anchored bottom-center over the sign, like the bubbles. */
.hero-sign {
  position: absolute; z-index: 4;
  transform: translate(-50%, -100%);
  pointer-events: none;
}
.hero-sign-card {
  width: max-content; max-width: 12rem; padding: 0.3rem 0.7rem 0.34rem;
  font: 600 0.95rem/1.25 "Fraunces", Georgia, serif; color: #2a2118;
  text-align: center;
  background: #f7eed8;
  border: 1px solid rgba(90, 64, 30, 0.35); border-radius: 6px;
  box-shadow: 0 4px 16px rgba(20, 35, 60, 0.28);
  transition: max-width 0.25s ease, padding 0.25s ease;
}
.hero-sign-title { font-weight: 700; letter-spacing: 0.01em; }
.hero-sign-body { display: none; }
.hero-sign-hint {
  margin-top: 0.1rem;
  font: 600 0.62rem/1.2 var(--mono); text-transform: uppercase; letter-spacing: 0.12em;
  color: rgba(42, 33, 24, 0.55);
}
/* Locks (hero-locks.js): the same paper chip over a shut padlock — its words and a hint —
   with a band in the lock's colour. */
.hero-lock-chip .hero-sign-card { max-width: 14rem; border-top: 3px solid var(--lock, #c9a15a); }
.hero-lock-chip .hero-sign-title { font-weight: 600; font-size: 0.88rem; }
/* The one-armed bandit (hero-slots.js): its card unfolds over the machine as a bird comes
   near — a saloon-red cabinet with a brass rim, three reels behind the window (each strip
   scrolls past and stops with an overshoot, the clunk), what three of a kind pays, and the
   pull button (F). A win lights the reels that made it; a unique or the jackpot glows. */
.hero-slots {
  position: absolute; z-index: 4;
  transform: translate(-50%, -100%);
  pointer-events: none;
  --cell: 44px;
}
.hero-slots-card {
  width: 12.6rem; padding: 0.45rem 0.6rem 0.55rem;
  font: 600 0.8rem/1.25 "Fraunces", Georgia, serif; color: #fbe9bf; text-align: center;
  background: linear-gradient(180deg, #8a2a1c 0%, #6d1e13 55%, #551710 100%);
  border: 2px solid #d9a441; border-radius: 12px 12px 8px 8px;
  box-shadow: inset 0 0 0 1px rgba(255, 228, 160, 0.25), 0 6px 20px rgba(20, 12, 6, 0.45);
}
.hero-slots-title {
  font: 700 0.78rem/1.1 "Fraunces", Georgia, serif; letter-spacing: 0.14em; text-transform: uppercase;
  color: #ffd36a; text-shadow: 0 1px 0 rgba(60, 20, 4, 0.8);
}
.hero-slots-window {
  position: relative;
  display: flex; justify-content: center; gap: 0.3rem;
  margin: 0.35rem 0 0.4rem; padding: 0.3rem;
  background: #2a120c; border: 1px solid #b7862f; border-radius: 6px;
  box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.65);
}
.hero-slots-reel {
  position: relative; overflow: hidden;
  width: 3.2rem; height: var(--cell);
  background: linear-gradient(180deg, #d9c9a6 0%, #fffaf0 26%, #fffaf0 74%, #d9c9a6 100%);
  border-radius: 4px;
  box-shadow: inset 0 0 6px rgba(70, 40, 10, 0.45);
  transition: box-shadow 0.2s ease;
}
.hero-slots-strip { will-change: transform; }
.hero-slots-cell {
  height: var(--cell);
  display: flex; align-items: center; justify-content: center;
  font-size: 1.55rem; line-height: 1;
  font-family: var(--hud-emoji, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif);
}
.hero-slots-cell img { width: 2.1rem; height: 2.1rem; object-fit: contain; }
.hero-slots-cell[data-sym="feather"] { color: #d99a12; }
.hero-slots-cell svg { width: 1.9rem; height: 1.9rem; filter: drop-shadow(0 1px 0 rgba(120, 70, 0, 0.35)); }
.hero-slots-reel.is-spin .hero-slots-strip { filter: blur(0.6px); }
.hero-slots-reel.is-stop { animation: hero-slots-stop 0.28s ease-out; }
.hero-slots-reel.is-win { box-shadow: inset 0 0 6px rgba(70, 40, 10, 0.45), 0 0 0 2px #ffd36a, 0 0 14px rgba(255, 211, 106, 0.95); }
.hero-slots-payline {
  position: absolute; left: 0.15rem; right: 0.15rem; top: 50%; height: 0;
  border-top: 1px dashed rgba(255, 90, 60, 0.55);
  pointer-events: none;
}
.hero-slots-pay { display: grid; gap: 0.05rem; margin-bottom: 0.4rem; }
.hero-slots-payrow { display: flex; align-items: center; justify-content: space-between; gap: 0.4rem; font-size: 0.7rem; }
.hero-slots-paysyms { display: inline-flex; gap: 0.05rem; }
.hero-slots-paysym {
  display: inline-flex; align-items: center; justify-content: center; width: 1rem; height: 1rem;
  font-size: 0.78rem; line-height: 1;
  font-family: var(--hud-emoji, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif);
}
.hero-slots-paysym img, .hero-slots-paysym svg { width: 0.95rem; height: 0.95rem; }
.hero-slots-paysym[data-sym="feather"] { color: #ffc94d; }
.hero-slots-paylabel { color: #fbe9bf; text-align: right; }
.hero-slots-line { min-height: 1.25em; margin-bottom: 0.35rem; font-size: 0.8rem; }
.hero-slots-line:empty { display: none; }
.hero-slots-line.is-spin { color: rgba(251, 233, 191, 0.7); font-style: italic; }
.hero-slots-line.is-win { color: #ffe08a; }
.hero-slots-line.is-big { color: #fff3c0; font-weight: 700; font-size: 0.92rem; text-shadow: 0 0 10px rgba(255, 210, 90, 0.85); }
.hero-slots-line.is-lose { color: rgba(251, 233, 191, 0.75); }
.hero-slots-line.is-denied { color: #ffb4a2; }
.hero-slots-pull {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem;
  width: 100%; padding: 0.38rem 0.6rem;
  font: 700 0.86rem/1 "Fraunces", Georgia, serif; color: #3a210a;
  background: linear-gradient(180deg, #ffe08a 0%, #e0a83a 100%);
  border: 1px solid #8a5a14; border-radius: 999px;
  box-shadow: 0 2px 0 #7a4c10, inset 0 1px 0 rgba(255, 255, 255, 0.6);
  cursor: pointer; pointer-events: auto;
}
.hero-slots-pull:hover { filter: brightness(1.06); }
.hero-slots-pull:active { transform: translateY(2px); box-shadow: 0 0 0 #7a4c10, inset 0 1px 0 rgba(255, 255, 255, 0.6); }
.hero-slots-pull:disabled { opacity: 0.55; cursor: default; }
.hero-slots.is-far .hero-slots-pull { opacity: 0.6; }
.hero-slots-key {
  display: inline-block; min-width: 1.15rem; padding: 0.08rem 0.25rem;
  font: 700 0.62rem/1 var(--mono); color: #3a210a;
  background: rgba(255, 250, 235, 0.75); border: 1px solid rgba(90, 55, 10, 0.5); border-radius: 4px;
}
.hero-slots.is-touch .hero-slots-key { display: none; }
.hero-slots.is-touch .hero-slots-pull { padding: 0.62rem 0.6rem; font-size: 0.95rem; }
.hero-slots.is-denied .hero-slots-card { animation: hero-slots-shake 0.4s ease; }
.hero-slots.is-jackpot .hero-slots-card { animation: hero-slots-glow 1.1s ease-in-out 3; }
@keyframes hero-slots-stop {
  0% { transform: translateY(-3px); }
  45% { transform: translateY(2px); }
  100% { transform: translateY(0); }
}
@keyframes hero-slots-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-6px); }
  40% { transform: translateX(5px); }
  60% { transform: translateX(-3px); }
  80% { transform: translateX(2px); }
}
@keyframes hero-slots-glow {
  0%, 100% { box-shadow: inset 0 0 0 1px rgba(255, 228, 160, 0.25), 0 6px 20px rgba(20, 12, 6, 0.45); }
  50% { box-shadow: inset 0 0 0 1px rgba(255, 228, 160, 0.6), 0 0 26px rgba(255, 205, 80, 0.95); }
}
@media (prefers-reduced-motion: reduce) {
  .hero-slots-reel.is-stop, .hero-slots.is-denied .hero-slots-card, .hero-slots.is-jackpot .hero-slots-card { animation: none; }
}
/* Chilled by a frost bolt (the Frost Gem — hero-game chill): the view's edges frost over and
   thaw away over the slow (--chill-s). */
.hero-chill {
  position: absolute; inset: 0; z-index: 3;
  pointer-events: none; opacity: 0;
  background: radial-gradient(ellipse at center, transparent 52%, rgba(190, 232, 255, 0.22) 76%, rgba(228, 246, 255, 0.62) 100%);
  box-shadow: inset 0 0 70px rgba(150, 215, 255, 0.55);
}
.hero-chill.is-on { animation: hero-chill var(--chill-s, 2s) ease-out forwards; }
@keyframes hero-chill {
  0% { opacity: 0; }
  8% { opacity: 1; }
  70% { opacity: 0.8; }
  100% { opacity: 0; }
}
/* The board already shows the title: closed, the tab is just the chip. */
.hero-sign.has-board:not(.is-open) .hero-sign-title { display: none; }
.hero-sign.has-board:not(.is-open) .hero-sign-card { padding: 0.24rem 0.55rem 0.26rem; }
.hero-sign.has-board:not(.is-open) .hero-sign-hint { margin-top: 0; }
.hero-sign.is-open .hero-sign-card {
  max-width: min(21rem, 80vw); padding: 0.7rem 0.95rem 0.8rem;
  text-align: left;
  pointer-events: auto; cursor: pointer; /* a click folds it away */
  animation: hero-sign-open 0.3s cubic-bezier(0.2, 1.4, 0.4, 1);
}
.hero-sign.is-open .hero-sign-title {
  margin-bottom: 0.35rem; padding-bottom: 0.3rem;
  font-size: 1.12rem; border-bottom: 1px solid rgba(90, 64, 30, 0.25);
}
.hero-sign.is-open .hero-sign-body { display: block; }
.hero-sign.is-open .hero-sign-hint { display: none; }
.hero-sign-body p {
  margin: 0 0 0.45rem;
  font: 500 0.9rem/1.4 "Fraunces", Georgia, serif; color: #3a2d1f;
}
.hero-sign-body p:last-child { margin-bottom: 0; }
@keyframes hero-sign-open {
  from { transform: scale(0.85); opacity: 0.4; }
}
/* The painted title: a fixed layout box (hero-sign.js LABEL_W × LABEL_H, font fitted to it)
   that an inline matrix() maps onto the board. Multiply keeps the wood's grain and light
   under the ink; a leading "The" sits small on its own line. */
.hero-sign-label {
  position: absolute; left: 0; top: 0; z-index: 3;
  display: flex; align-items: center; justify-content: center;
  transform-origin: 0 0;
  color: #3d2410; text-align: center;
  mix-blend-mode: multiply;
  pointer-events: none;
  transition: opacity 0.2s linear;
}
.hero-sign-label > span {
  display: inline-block; max-width: 96%;
  font: 700 48px/0.98 "Fraunces", Georgia, serif; letter-spacing: 0.005em;
  text-wrap: balance;
}
.hero-sign-label small {
  display: block; margin-bottom: 0.08em;
  font-size: 0.4em; font-weight: 700; line-height: 1; letter-spacing: 0.2em; text-transform: uppercase;
}
/* Online status, top-right (below the floating nav): how many other birds share the
   meadow. Only shown with company, and only while the name block (and the presence line in
   it) is faded out for play; a live dot, and the trailing "!" wobbles. */
.hero-online {
  position: absolute; top: 5.2rem; right: 14px; z-index: 3;
  display: flex; align-items: center;
  padding: 0.35rem 0.65rem;
  background: var(--hud-bg); color: var(--hud-ink);
  border: 1px solid var(--hud-line); border-radius: 6px;
  font-family: "Overused Grotesk", system-ui, sans-serif; font-size: 0.76rem; font-weight: 600;
  white-space: nowrap; cursor: pointer; line-height: inherit; margin: 0;
}
.hero-online:hover, .hero-online[aria-expanded="true"] { border-color: rgba(255, 255, 255, 0.4); }
/* The chip's list (hero-presence refreshList): who is around — birds in this world with the
   direction from ours (north is up on the screen), birds elsewhere with their world. */
.hero-online-list {
  position: absolute; top: 7.5rem; right: 14px; z-index: 3;
  min-width: 11rem; max-width: min(18rem, calc(100% - 28px));
  padding: 0.45rem 0.65rem 0.5rem;
  background: var(--hud-bg); color: var(--hud-ink);
  border: 1px solid var(--hud-line); border-radius: 6px;
  font-family: "Overused Grotesk", system-ui, sans-serif; font-size: 0.76rem; line-height: 1.45;
}
.hero-online-list[hidden] { display: none; }
.hero-online-list-title {
  margin-bottom: 0.2rem; font-size: 0.66rem; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.65;
}
.hero-online-row b { font-weight: 700; }
.hero-online-row.is-none { opacity: 0.7; }
/* Keys found (hero-game, gameplay.md §37): top-left, the online chip's twin — a key, "1/4",
   "keys found" and a dot per key in its own colour (filled once this bird has held it). It pops
   when a new key counts and turns gold when they are all found. Shown from the first move on. */
.hero-keys {
  position: absolute; top: 5.2rem; left: 14px; z-index: 3;
  display: flex; align-items: center; gap: 0.35rem;
  padding: 0.32rem 0.6rem;
  background: var(--hud-bg); color: var(--hud-ink);
  border: 1px solid var(--hud-line); border-radius: 6px;
  font-family: "Overused Grotesk", system-ui, sans-serif; font-size: 0.76rem; font-weight: 600;
  white-space: nowrap; pointer-events: auto; cursor: default;
}
.hero-keys[hidden] { display: none; }
.hero-keys-icon { display: inline-flex; width: 1rem; height: 1rem; color: var(--hud-gold, #ffd36a); }
.hero-keys-icon svg { width: 100%; height: 100%; }
.hero-keys-num { font-weight: 700; font-variant-numeric: tabular-nums; }
.hero-keys-label { opacity: 0.8; font-weight: 500; }
.hero-keys-dots { display: inline-flex; gap: 0.2rem; margin-left: 0.1rem; }
.hero-keys-dot {
  width: 0.5rem; height: 0.5rem; border-radius: 50%;
  border: 1.5px solid var(--key, #c9a15a); background: transparent; opacity: 0.75;
}
.hero-keys-dot.is-found { background: var(--key, #c9a15a); opacity: 1; box-shadow: 0 0 5px var(--key, #c9a15a); }
.hero-keys.is-all { border-color: rgba(255, 211, 106, 0.75); box-shadow: 0 0 10px rgba(255, 211, 106, 0.45); }
.hero-keys.is-new { animation: hero-keys-pop 0.7s cubic-bezier(0.2, 1.5, 0.4, 1); }
@keyframes hero-keys-pop {
  0% { transform: scale(1); }
  30% { transform: scale(1.22); border-color: rgba(255, 211, 106, 0.9); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) { .hero-keys.is-new { animation: none; } }
/* Notices (hero-presence notice): "Crow flew in! West of you." — top centre, a few seconds. */
.hero-notices {
  position: absolute; top: 5.2rem; left: 50%; z-index: 3; transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 0.35rem;
  width: max-content; max-width: calc(100% - 28px); pointer-events: none;
}
.hero-notice {
  padding: 0.35rem 0.75rem;
  background: var(--hud-bg); color: var(--hud-ink);
  border: 1px solid var(--hud-line); border-radius: 6px;
  font-family: "Overused Grotesk", system-ui, sans-serif; font-size: 0.8rem; font-weight: 600;
  text-align: center; animation: hero-notice-in 0.35s ease both;
  transition: opacity 0.45s ease, transform 0.45s ease;
}
.hero-notice.is-out { opacity: 0; transform: translateY(-6px); }
@keyframes hero-notice-in {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: none; }
}
.hero-online::before {
  content: ""; width: 7px; height: 7px; margin-right: 0.45rem; border-radius: 50%;
  background: #7ed07e; box-shadow: 0 0 6px rgba(126, 208, 126, 0.8);
}
.hero-online[hidden] { display: none; }
.hero-online-bang {
  display: inline-block; margin-left: 0.1rem;
  color: #ffd76e; transform-origin: 50% 90%;
  animation: hero-online-wobble 1.1s ease-in-out infinite;
}
@keyframes hero-online-wobble {
  0%, 100% { transform: rotate(0deg); }
  15% { transform: rotate(-16deg); }
  30% { transform: rotate(12deg); }
  45% { transform: rotate(-9deg); }
  60% { transform: rotate(6deg); }
  75% { transform: rotate(-3deg); }
}

/* Meadow game HUD (hero-game.js / hero-hud.js): the level bar bottom-left (jump circles
   above it), the dock bottom-right (carried item + skills, left of the ⚙ chip), the
   level-up screen over the middle, and the collect toasts (the rise-and-dissolve above).
   Bottom-center belongs to the chat. The wrap is a click-through layer; slots opt back
   into pointer events. */
.hero-hud {
  position: absolute; inset: 0; z-index: 3;
  pointer-events: none;
  font-family: "Overused Grotesk", system-ui, sans-serif; color: var(--hud-ink);
}
/* display:flex out-specifies the UA [hidden]{display:none} — restate it (the lightbox
   rule's lesson), or hidden groups render as empty boxes. */
.hero-hud[hidden], .hero-level[hidden], .hero-dock[hidden], .hero-dock-group[hidden], .hero-item-tip[hidden] { display: none; }
.hero-level {
  --lvl-x: 0px; /* the bump keyframes' base offset — bottom-left, nothing to center */
  position: absolute; left: 12px; bottom: 12px;
  width: min(300px, 23vw);
  display: flex; align-items: center; gap: 10px;
  padding: 6px 12px 7px 6px;
  background: var(--hud-bg);
  border: 1px solid var(--hud-line); border-radius: var(--hud-radius);
  box-shadow: var(--hud-shadow);
}
.hero-level-badge {
  flex: none; width: 38px; height: 38px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  border-radius: 6px; color: var(--hud-gold);
  background: linear-gradient(180deg, rgba(255, 215, 110, 0.22), rgba(255, 215, 110, 0.06));
  border: 1px solid rgba(255, 215, 110, 0.6);
}
.hero-level-cap {
  font-size: 0.5rem; font-weight: 700; line-height: 1;
  text-transform: uppercase; letter-spacing: 0.14em; opacity: 0.85;
}
.hero-level-num { font: 800 1.25rem/1 "Fraunces", Georgia, serif; margin-top: 1px; }
.hero-level-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.hero-level-row {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  font-size: 0.74rem; font-weight: 600; white-space: nowrap;
}
.hero-level-tally { display: inline-flex; align-items: center; gap: 4px; }
.hero-level-count { font: 700 0.98rem/1 "Fraunces", Georgia, serif; }
.hero-feather { display: inline-flex; width: 15px; height: 15px; color: var(--hud-gold); }
.hero-feather svg { width: 100%; height: 100%; }
.hero-level-next { min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--hud-dim); }
.hero-level-track {
  height: 7px; border-radius: 3px; overflow: hidden;
  background: rgba(255, 255, 255, 0.1);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.35);
}
.hero-level-fill {
  display: block; height: 100%; width: 0; border-radius: 3px;
  background: linear-gradient(90deg, #d99a2b, var(--hud-gold));
  box-shadow: 0 0 8px rgba(255, 215, 110, 0.45);
  transition: width 0.6s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.hero-level.is-max .hero-level-fill { background: linear-gradient(90deg, var(--hud-gold), #fff1c2); }
/* The level bar's reactions (hero-hud.update fx): every XP orb that lands jolts the panel
   (.is-bump; gold feathers .is-bump-big), flashes the fill (.is-tick) and pops the count; a
   level change fills it to the brim with a glow (.is-full), then the badge flips (.is-flip).
   Every keyframe restates the panel's centering offset (--lvl-x; 0 on narrow heroes, where
   the bar spans the bottom) — transform animations replace. */
.hero-level-unit {
  margin-left: 2px; font-size: 0.6rem; font-weight: 800; letter-spacing: 0.1em; color: var(--hud-dim);
}
.hero-level-count { display: inline-block; }
.hero-level-count.is-count { animation: hero-count-pop 0.3s cubic-bezier(0.2, 1.8, 0.4, 1); }
@keyframes hero-count-pop {
  0% { transform: scale(1.45); color: #fff; text-shadow: 0 0 10px rgba(255, 230, 150, 0.9); }
  100% { transform: scale(1); }
}
.hero-level-track { position: relative; }
.hero-level-shine {
  position: absolute; left: 0; right: 0; top: 0; height: 45%; pointer-events: none;
  background: linear-gradient(rgba(255, 255, 255, 0.2), transparent); border-radius: 3px 3px 0 0;
}
.hero-level-fill { position: relative; overflow: hidden; transition: width 0.34s cubic-bezier(0.34, 1.56, 0.64, 1); }
/* The idle glint sweeping along the fill every few seconds. */
.hero-level-fill::after {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 40%;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent);
  transform: translateX(-110%);
  animation: hero-fill-glint 3.4s ease-in-out 1.2s infinite;
}
@keyframes hero-fill-glint {
  0% { transform: translateX(-110%); }
  45%, 100% { transform: translateX(360%); }
}
.hero-level-fill.is-tick { animation: hero-fill-tick 0.38s ease-out; }
@keyframes hero-fill-tick {
  0% { filter: brightness(2.3) saturate(1.3); box-shadow: 0 0 16px rgba(255, 240, 190, 1); }
  100% { filter: none; }
}
.hero-level.is-bump { animation: hero-level-bump 0.3s cubic-bezier(0.36, 0.07, 0.19, 0.97); }
.hero-level.is-bump-big { animation: hero-level-bump-big 0.42s cubic-bezier(0.36, 0.07, 0.19, 0.97); }
@keyframes hero-level-bump {
  0% { transform: translateX(var(--lvl-x)); }
  25% { transform: translateX(calc(var(--lvl-x) - 3px)) translateY(-2px) rotate(-0.8deg) scale(1.025); }
  55% { transform: translateX(calc(var(--lvl-x) + 3px)) rotate(0.6deg) scale(1.02); }
  80% { transform: translateX(calc(var(--lvl-x) - 1px)) rotate(-0.2deg); }
  100% { transform: translateX(var(--lvl-x)); }
}
@keyframes hero-level-bump-big {
  0% { transform: translateX(var(--lvl-x)); }
  20% { transform: translateX(calc(var(--lvl-x) - 5px)) translateY(-4px) rotate(-1.6deg) scale(1.05); }
  45% { transform: translateX(calc(var(--lvl-x) + 5px)) translateY(-1px) rotate(1.2deg) scale(1.04); }
  70% { transform: translateX(calc(var(--lvl-x) - 2px)) rotate(-0.5deg) scale(1.01); }
  100% { transform: translateX(var(--lvl-x)); }
}
.hero-level.is-full { animation: hero-level-full 0.95s ease-out; }
@keyframes hero-level-full {
  0% { transform: translateX(var(--lvl-x)) scale(1); }
  18% { transform: translateX(var(--lvl-x)) scale(1.08); border-color: #ffe9a8;
    box-shadow: 0 0 0 2px rgba(255, 215, 110, 0.7), 0 0 34px rgba(255, 205, 90, 0.85); }
  100% { transform: translateX(var(--lvl-x)) scale(1); }
}
.hero-level-badge.is-flip { animation: hero-badge-flip 0.65s cubic-bezier(0.2, 1.6, 0.4, 1); }
@keyframes hero-badge-flip {
  0% { transform: perspective(200px) rotateY(90deg) scale(1.3); }
  100% { transform: perspective(200px) rotateY(0) scale(1); }
}

/* XP orbs (hero-hud.xpOrbs): glowing spheres flying from the feather into the bar, each
   with two fading ghosts behind it; tinted by feather tier. WAAPI drives the transform. */
.hero-xp--black { --orb: #9f84ff; }
.hero-xp--silver { --orb: #e6eeff; }
.hero-xp--gold { --orb: #ffcf3a; }
.hero-xp--spend { --orb: #ff8a6a; } /* XP paid out (a repair's cost): the "−2" off the bar */
.hero-xp-orb {
  position: absolute; left: 0; top: 0; z-index: 6;
  width: 15px; height: 15px; border-radius: 50%;
  pointer-events: none; will-change: transform;
  background: radial-gradient(circle at 34% 30%, #fff 0 15%, var(--orb) 46%, color-mix(in srgb, var(--orb) 55%, #2b1a00) 100%);
  box-shadow: 0 0 8px 2px var(--orb), 0 0 20px 5px color-mix(in srgb, var(--orb) 40%, transparent);
}
.hero-xp-orb.hero-xp--black {
  background: radial-gradient(circle at 34% 30%, #f1ebff 0 13%, #6b4ee0 44%, #170d2e 100%);
}
.hero-xp-orb.is-ghost { box-shadow: 0 0 10px 2px color-mix(in srgb, var(--orb) 60%, transparent); }
.hero-xp-orb.is-ghost-1 { width: 10px; height: 10px; opacity: 0.55; }
.hero-xp-orb.is-ghost-2 { width: 7px; height: 7px; opacity: 0.28; }
/* "+1" rising off the bar where an orb landed, and the ring it leaves. */
.hero-xp-float {
  position: absolute; z-index: 6; pointer-events: none; white-space: nowrap;
  transform: translate(-50%, -50%);
  font: 800 1.05rem/1 "Fraunces", Georgia, serif; color: var(--orb);
  text-shadow: 0 1px 5px rgba(0, 0, 0, 0.6), 0 0 12px var(--orb);
  animation: hero-xp-float 1s cubic-bezier(0.2, 0.8, 0.3, 1) forwards;
}
@keyframes hero-xp-float {
  0% { transform: translate(-50%, -30%) scale(0.4); opacity: 0; }
  18% { transform: translate(-50%, -95%) scale(1.3); opacity: 1; }
  100% { transform: translate(calc(-50% + var(--dx, 0px)), -290%) scale(0.9); opacity: 0; }
}
.hero-xp-spark {
  position: absolute; z-index: 6; pointer-events: none;
  width: 26px; height: 26px; margin: -13px 0 0 -13px; border-radius: 50%;
  border: 2px solid var(--orb); box-shadow: 0 0 10px var(--orb), inset 0 0 8px var(--orb);
  animation: hero-xp-spark 0.45s ease-out forwards;
}
@keyframes hero-xp-spark {
  from { transform: scale(0.2); opacity: 1; }
  to { transform: scale(1.7); opacity: 0; }
}

/* Letter-by-letter lines (hero-hud letters()): each letter is its own inline-block with --i. */
.hero-letters { display: inline-block; white-space: pre; }
.hero-letter { display: inline-block; }
@keyframes hero-letter-hop {
  0% { transform: translateY(0.55em) scale(0.2) rotate(-12deg); opacity: 0; }
  60% { opacity: 1; }
  100% { transform: none; opacity: 1; }
}
@keyframes hero-letter-drop {
  0% { transform: translateY(-1.2em) scale(1.6); opacity: 0; }
  100% { transform: none; opacity: 1; }
}
@keyframes hero-letter-wave {
  0%, 60%, 100% { transform: translateY(0); }
  30% { transform: translateY(-0.18em); }
}

/* The LEVEL-UP SCREEN (hero-hud.levelUp): a warm glow + slowly turning sunburst behind a dark
   card; "Level up!" drops in letter by letter and keeps waving, the number punches in with a
   moving gold shimmer and floats, "+1 jump gained" hops in, the jump pips pop one by one (the
   new one rings), sparks fly out. Click-through; the whole thing fades at ~3.9 s. */
.hero-lvlup {
  position: absolute; inset: 0; z-index: 7;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; pointer-events: none;
  font-family: "Overused Grotesk", system-ui, sans-serif; color: var(--hud-ink);
  animation: hero-lvlup-out 0.55s ease-in 3.9s forwards;
}
.hero-lvlup-glow {
  position: absolute; inset: -10%;
  background:
    radial-gradient(ellipse 50% 42% at 50% 44%, rgba(255, 205, 90, 0.42), rgba(255, 170, 40, 0.14) 45%, transparent 72%),
    radial-gradient(ellipse at 50% 46%, rgba(16, 12, 6, 0.5), transparent 78%);
  animation: hero-lvlup-fade 0.35s ease-out both, hero-lvlup-pulse 1.8s ease-in-out 0.4s infinite;
}
.hero-lvlup-rays {
  position: absolute; left: 50%; top: 44%;
  width: 150vmax; height: 150vmax; margin: -75vmax 0 0 -75vmax;
  background: repeating-conic-gradient(from 0deg, rgba(255, 228, 150, 0.2) 0deg 6deg, transparent 6deg 18deg);
  -webkit-mask: radial-gradient(circle, #000 0, rgba(0, 0, 0, 0.55) 16%, transparent 40%);
  mask: radial-gradient(circle, #000 0, rgba(0, 0, 0, 0.55) 16%, transparent 40%);
  animation: hero-lvlup-fade 0.6s ease-out both, hero-lvlup-spin 18s linear infinite;
}
.hero-lvlup-card {
  position: relative; margin-top: -8vh;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  min-width: min(400px, 86%); padding: 20px 34px 18px; text-align: center;
  background: linear-gradient(180deg, rgba(38, 30, 17, 0.97), rgba(20, 17, 12, 0.97));
  border: 1px solid rgba(255, 215, 110, 0.6); border-radius: 12px;
  box-shadow: 0 18px 50px rgba(10, 14, 24, 0.45), 0 0 70px rgba(255, 196, 70, 0.32),
    inset 0 1px 0 rgba(255, 255, 255, 0.1);
  animation: hero-lvlup-card-in 0.7s cubic-bezier(0.18, 1.5, 0.4, 1) both;
}
.hero-lvlup-kicker {
  font-size: 0.86rem; font-weight: 800; letter-spacing: 0.34em; text-transform: uppercase;
  color: #fff1c0; text-shadow: 0 0 12px rgba(255, 200, 80, 0.7);
}
.hero-lvlup-kicker .hero-letter {
  animation: hero-letter-drop 0.5s cubic-bezier(0.2, 1.7, 0.4, 1) both, hero-letter-wave 1.5s ease-in-out infinite;
  animation-delay: calc(0.12s + var(--i) * 45ms), calc(1.1s + var(--i) * 70ms);
}
.hero-lvlup-level { display: flex; align-items: baseline; gap: 0.28em; line-height: 1; margin: 2px 0 2px; }
.hero-lvlup-word {
  font: 700 1.7rem/1 "Fraunces", Georgia, serif; color: #f4e7c6;
  animation: hero-lvlup-rise 0.45s ease-out 0.22s both;
}
.hero-lvlup-num {
  display: inline-block;
  font: 900 clamp(4.4rem, 11vw, 6.6rem)/0.9 "Fraunces", Georgia, serif;
  background: linear-gradient(100deg, #ffac24 0%, #ffe38a 28%, #ffffff 42%, #ffe07a 56%, #ffab22 100%);
  background-size: 260% 100%;
  -webkit-background-clip: text; background-clip: text; color: transparent;
  filter: drop-shadow(0 3px 0 #7a4a08) drop-shadow(0 0 18px rgba(255, 196, 70, 0.6));
  animation: hero-lvlup-num-in 0.8s cubic-bezier(0.2, 1.8, 0.4, 1) 0.3s both,
    hero-shimmer 2.3s linear 0.9s infinite, hero-lvlup-float 2.4s ease-in-out 1.1s infinite;
}
.hero-lvlup.is-max .hero-lvlup-num {
  background-image: linear-gradient(100deg, #ff8a3d, #ffe066 22%, #7af0ff 45%, #c79bff 68%, #ff8a3d);
}
.hero-lvlup-perk {
  width: 100%; margin-top: 8px; padding-top: 12px;
  display: flex; flex-direction: column; align-items: center; gap: 9px;
  border-top: 1px solid rgba(255, 215, 110, 0.22);
}
.hero-lvlup-gain {
  font: 800 clamp(1.55rem, 4vw, 2.05rem)/1 "Fraunces", Georgia, serif;
  color: #9ff1ff; text-shadow: 0 2px 0 #0d4a5a, 0 0 16px rgba(120, 230, 255, 0.65);
}
.hero-lvlup-gain .hero-letter {
  animation: hero-letter-hop 0.5s cubic-bezier(0.2, 1.8, 0.4, 1) both, hero-letter-wave 1.8s ease-in-out infinite;
  animation-delay: calc(0.7s + var(--i) * 32ms), calc(1.6s + var(--i) * 60ms);
}
.hero-lvlup-pips { display: flex; gap: 9px; }
.hero-lvlup-pip {
  position: relative; width: 16px; height: 16px; border-radius: 50%;
  background: var(--hud-gold); border: 1px solid #e9c46a; box-shadow: 0 0 8px rgba(233, 196, 106, 0.6);
  animation: hero-pip-in 0.4s cubic-bezier(0.2, 1.8, 0.4, 1) both;
  animation-delay: calc(0.85s + var(--i) * 80ms);
}
.hero-lvlup-pip.is-new {
  background: #a6f4ff; border-color: #e0fcff; box-shadow: 0 0 14px rgba(140, 240, 255, 0.95);
  animation-name: hero-pip-in-big; animation-duration: 0.6s;
  animation-delay: calc(1.05s + var(--i) * 80ms);
}
.hero-lvlup-pip.is-new::after {
  content: ""; position: absolute; inset: -6px; border-radius: 50%;
  border: 2px solid #a6f4ff; opacity: 0;
  animation: hero-pip-ring 1.1s ease-out calc(1.45s + var(--i) * 80ms) infinite;
}
.hero-lvlup-sub {
  font-size: 0.88rem; font-weight: 600; color: rgba(242, 236, 223, 0.88);
  animation: hero-lvlup-rise 0.5s ease-out 1.25s both;
}
.hero-lvlup-minor {
  margin-top: 4px; font-size: 0.74rem; font-weight: 700; letter-spacing: 0.05em; color: var(--hud-dim);
  animation: hero-lvlup-rise 0.5s ease-out 1.5s both;
}
.hero-lvlup-crown { color: var(--hud-gold); }
/* The level's own reward under the jump (level 4: the hat, level 5: the glide). */
.hero-lvlup-reward {
  display: flex; align-items: center; gap: 12px;
  margin-top: 10px; padding: 9px 16px 9px 10px;
  border-radius: 10px; text-align: left;
  background: linear-gradient(180deg, rgba(255, 215, 110, 0.16), rgba(255, 215, 110, 0.05));
  border: 1px solid rgba(255, 215, 110, 0.5);
  animation: hero-lvlup-rise 0.5s ease-out 1.35s both;
}
.hero-lvlup-reward-mark {
  flex: none; width: 34px; height: 34px; color: var(--hud-gold);
  filter: drop-shadow(0 0 8px rgba(255, 200, 80, 0.7));
  animation: hero-lvlup-num-in 0.7s cubic-bezier(0.2, 1.8, 0.4, 1) 1.45s both, hero-lvlup-float 2.4s ease-in-out 2.2s infinite;
}
.hero-lvlup-reward-mark svg { width: 100%; height: 100%; }
.hero-lvlup-reward-head {
  display: block; font: 800 1.2rem/1.1 "Fraunces", Georgia, serif; color: #ffe28a;
  text-shadow: 0 2px 0 #6a3f06, 0 0 12px rgba(255, 196, 70, 0.55);
}
.hero-lvlup-reward-head .hero-letter {
  animation: hero-letter-hop 0.5s cubic-bezier(0.2, 1.8, 0.4, 1) both;
  animation-delay: calc(1.5s + var(--i) * 28ms);
}
.hero-lvlup-reward-sub { margin-top: 3px; font-size: 0.8rem; font-weight: 600; color: rgba(242, 236, 223, 0.85); }

/* The level bar's teaser: what the next level brings besides its jump. */
.hero-level-tease { color: var(--hud-gold); font-weight: 700; }

/* "Pick your hat" — on the level bar while the level-4 choice is put off. */
.hero-hatchip {
  position: absolute; right: 8px; bottom: calc(100% + 8px);
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 11px 5px 7px; border-radius: 999px;
  font: 700 0.74rem/1 "Overused Grotesk", system-ui, sans-serif; color: #2a1c05;
  background: linear-gradient(180deg, #ffe08a, #f2b843);
  border: 1px solid #fff0c0; box-shadow: 0 4px 14px rgba(255, 190, 60, 0.45);
  cursor: pointer; pointer-events: auto; white-space: nowrap;
  animation: hero-hatchip-nudge 2.6s ease-in-out 1s infinite;
}
.hero-hatchip[hidden] { display: none; }
.hero-hatchip svg { width: 17px; height: 17px; }
.hero-hatchip:hover { filter: brightness(1.06); }
@keyframes hero-hatchip-nudge {
  0%, 70%, 100% { transform: none; }
  78% { transform: translateY(-3px) rotate(-4deg); }
  86% { transform: translateY(0) rotate(3deg); }
}

/* The HAT PICKER (hero-hud.hatPicker): top-center, just under the floating nav, so the bird
   wearing the tried-on hat stays in view below it. Three hat cards (tap = try on), then
   Later / Keep. */
.hero-hatpick {
  position: absolute; left: 0; right: 0; top: 4.4rem; z-index: 8;
  display: flex; justify-content: center;
  padding: 0 16px; pointer-events: none;
  font-family: "Overused Grotesk", system-ui, sans-serif; color: var(--hud-ink);
}
.hero-hatpick-card {
  pointer-events: auto;
  width: min(560px, 100%);
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 14px 16px 14px; text-align: center;
  background: linear-gradient(180deg, rgba(38, 30, 17, 0.95), rgba(20, 17, 12, 0.95));
  border: 1px solid rgba(255, 215, 110, 0.6); border-radius: 12px;
  box-shadow: 0 18px 50px rgba(10, 14, 24, 0.45), 0 0 50px rgba(255, 196, 70, 0.22),
    inset 0 1px 0 rgba(255, 255, 255, 0.1);
  animation: hero-hatpick-in 0.55s cubic-bezier(0.18, 1.4, 0.4, 1) both;
}
.hero-hatpick-kicker {
  font-size: 0.7rem; font-weight: 800; letter-spacing: 0.3em; text-transform: uppercase;
  color: #fff1c0; text-shadow: 0 0 10px rgba(255, 200, 80, 0.6);
}
.hero-hatpick-kicker .hero-letter {
  animation: hero-letter-drop 0.45s cubic-bezier(0.2, 1.7, 0.4, 1) both;
  animation-delay: calc(0.1s + var(--i) * 30ms);
}
.hero-hatpick-title { font: 800 1.55rem/1.1 "Fraunces", Georgia, serif; color: #ffe28a; text-shadow: 0 2px 0 #6a3f06; }
.hero-hatpick-sub { font-size: 0.8rem; font-weight: 600; color: var(--hud-dim); }
.hero-hatpick-row {
  width: 100%; margin-top: 8px;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px;
}
.hero-hatpick-hat {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 10px 8px 9px; min-width: 0;
  font: inherit; color: inherit; text-align: center; cursor: pointer;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--hud-line); border-radius: 10px;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
  animation: hero-lvlup-rise 0.45s ease-out both;
  animation-delay: calc(0.25s + var(--i) * 90ms);
}
.hero-hatpick-hat:hover { background: rgba(255, 215, 110, 0.1); border-color: rgba(255, 215, 110, 0.55); transform: translateY(-2px); }
.hero-hatpick-hat.is-on {
  background: linear-gradient(180deg, rgba(255, 215, 110, 0.26), rgba(255, 215, 110, 0.08));
  border-color: var(--hud-gold); box-shadow: 0 0 0 1px var(--hud-gold), 0 0 18px rgba(255, 200, 80, 0.4);
}
.hero-hatpick-icon {
  width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;
  color: var(--hud-gold);
}
.hero-hatpick-icon img { width: 100%; height: 100%; object-fit: contain; }
.hero-hatpick-icon svg { width: 72%; height: 72%; }
.hero-hatpick-name { font: 700 0.95rem/1.15 "Fraunces", Georgia, serif; color: var(--hud-ink); }
.hero-hatpick-line { font-size: 0.72rem; line-height: 1.3; color: var(--hud-dim); }
.hero-hatpick-actions { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 10px; }
.hero-hatpick-later, .hero-hatpick-keep {
  font: 700 0.84rem/1 "Overused Grotesk", system-ui, sans-serif; cursor: pointer;
  padding: 9px 14px; border-radius: 999px;
}
.hero-hatpick-later { color: var(--hud-dim); background: none; border: 1px solid var(--hud-line); }
.hero-hatpick-later:hover { color: var(--hud-ink); }
.hero-hatpick-keep {
  color: #2a1c05; background: linear-gradient(180deg, #ffe08a, #f2b843);
  border: 1px solid #fff0c0; box-shadow: 0 4px 14px rgba(255, 190, 60, 0.4);
}
.hero-hatpick-keep:disabled { cursor: default; color: var(--hud-dim); background: rgba(255, 255, 255, 0.06); border-color: var(--hud-line); box-shadow: none; }
@keyframes hero-hatpick-in {
  0% { transform: translateY(-24px) scale(0.92); opacity: 0; }
  100% { transform: none; opacity: 1; }
}
@media (max-width: 600px) {
  .hero-hatpick { top: 3.6rem; padding: 0 8px; }
  .hero-hatpick-card { padding: 10px 10px 11px; }
  .hero-hatpick-title { font-size: 1.25rem; }
  .hero-hatpick-sub { font-size: 0.72rem; }
  .hero-hatpick-icon { width: 46px; height: 46px; }
  .hero-hatpick-line { display: none; }
  .hero-hatpick-name { font-size: 0.8rem; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-lvlup-reward, .hero-lvlup-reward-mark, .hero-hatchip, .hero-hatpick-card, .hero-hatpick-hat { animation: none; }
}
.hero-lvlup-sparks { position: absolute; left: 50%; top: 40%; width: 0; height: 0; }
.hero-lvlup-spark {
  position: absolute; width: 12px; height: 12px; margin: -6px;
  background: #fff4b8;
  clip-path: polygon(50% 0, 62% 38%, 100% 50%, 62% 62%, 50% 100%, 38% 62%, 0 50%, 38% 38%);
  filter: drop-shadow(0 0 6px #ffd23f);
  animation: hero-lvlup-spark 1.15s cubic-bezier(0.15, 0.8, 0.3, 1) 0.3s both;
}
@keyframes hero-lvlup-fade { from { opacity: 0; } }
@keyframes hero-lvlup-pulse { 50% { opacity: 0.78; } }
@keyframes hero-lvlup-spin { to { transform: rotate(360deg); } }
@keyframes hero-lvlup-card-in {
  0% { transform: scale(0.3) rotate(-5deg); opacity: 0; }
  55% { opacity: 1; }
  100% { transform: none; opacity: 1; }
}
@keyframes hero-lvlup-num-in {
  0% { transform: scale(2.6) rotate(-10deg); opacity: 0; }
  100% { transform: none; opacity: 1; }
}
@keyframes hero-shimmer { from { background-position: 100% 0; } to { background-position: -160% 0; } }
@keyframes hero-lvlup-float {
  0%, 100% { transform: translateY(0) rotate(0); }
  50% { transform: translateY(-5px) rotate(-1.5deg); }
}
@keyframes hero-lvlup-rise { from { transform: translateY(10px); opacity: 0; } }
@keyframes hero-pip-in { from { transform: scale(0); opacity: 0; } }
@keyframes hero-pip-in-big {
  0% { transform: scale(0); opacity: 0; }
  60% { transform: scale(1.7); opacity: 1; }
  100% { transform: scale(1); }
}
@keyframes hero-pip-ring {
  0% { transform: scale(0.6); opacity: 0.9; }
  100% { transform: scale(1.8); opacity: 0; }
}
@keyframes hero-lvlup-spark {
  0% { transform: rotate(var(--a)) translateX(0) scale(0); opacity: 1; }
  70% { opacity: 1; }
  100% { transform: rotate(var(--a)) translateX(var(--d)) rotate(200deg) scale(var(--s)); opacity: 0; }
}
@keyframes hero-lvlup-out { to { opacity: 0; transform: scale(1.04); } }
@media (max-width: 600px) {
  .hero-lvlup-card { min-width: 0; width: 88%; padding: 16px 16px 14px; }
  .hero-lvlup-kicker { letter-spacing: 0.24em; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-lvlup-rays, .hero-lvlup-sparks, .hero-xp-float, .hero-xp-spark { display: none; }
  .hero-lvlup-glow, .hero-lvlup-card, .hero-lvlup-word, .hero-lvlup-num, .hero-lvlup-sub, .hero-lvlup-minor,
  .hero-lvlup-pip, .hero-lvlup-pip::after, .hero-letter, .hero-level-fill::after,
  .hero-level.is-bump, .hero-level.is-bump-big, .hero-level.is-full, .hero-level-badge.is-flip,
  .hero-level-count.is-count, .hero-level-fill.is-tick { animation: none; }
  .hero-level-fill { transition: none; }
}
/* Jumps-left circles: just above the level bar. Opacity-driven (never [hidden] — see the
   flex note above); one dot per jump, lit while still available. */
.hero-jumps {
  position: absolute; left: calc(12px + min(300px, 23vw) / 2); bottom: 68px; z-index: 3;
  transform: translateX(-50%);
  display: flex; gap: 6px;
  pointer-events: none;
  opacity: 0; transition: opacity 0.25s ease;
}
.hero-jumps.is-live { opacity: 1; }
.hero-jump-pip {
  width: 10px; height: 10px; border-radius: 50%;
  background: rgba(22, 20, 17, 0.7); border: 1px solid rgba(255, 255, 255, 0.3);
  transition: background 0.12s ease, border-color 0.12s ease;
}
.hero-jump-pip.is-on {
  background: var(--hud-gold); border-color: #e9c46a;
  box-shadow: 0 0 6px rgba(233, 196, 106, 0.5);
}
/* Dock (Georg's mock, 2026-10-01): two framed cards under gold captions — EQUIPPED (the
   carried item) and SKILLS (a tile per skill it grants). Dark, warm cards with a thin gold
   rim; names in Fraunces, the rest in Overused Grotesk. */
.hero-dock {
  --dock-gold: #e2bd66;
  --dock-rim: rgba(214, 178, 94, 0.55);
  --dock-card: linear-gradient(180deg, rgba(40, 35, 26, 0.9), rgba(25, 22, 17, 0.88));
  position: absolute; right: 56px; bottom: 12px;
  display: flex; align-items: stretch; gap: 12px;
}
.hero-dock-group { display: flex; flex-direction: column; align-items: stretch; gap: 6px; }
.hero-dock-cap {
  display: flex; align-items: center; gap: 7px;
  padding-left: 2px;
  font-size: 0.66rem; font-weight: 800; line-height: 1;
  text-transform: uppercase; letter-spacing: 0.16em;
  color: rgba(255, 252, 244, 0.94); text-shadow: var(--hud-text-shadow);
}
.hero-dock-cap-glyph { display: inline-flex; color: var(--dock-gold); filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.45)); }
.hero-dock-cap-glyph svg { width: 15px; height: 15px; }
.hero-dock-cap-rule {
  flex: 1; min-width: 16px; height: 1.5px; border-radius: 1px;
  background: linear-gradient(90deg, rgba(226, 189, 102, 0.85), rgba(226, 189, 102, 0.15));
}
.hero-item-card, .hero-skill-row {
  flex: 1; /* both cards share the taller one's height, so the captions line up */
  display: flex;
  background: var(--dock-card);
  border: 1.5px solid var(--dock-rim); border-radius: 12px;
  box-shadow: var(--hud-shadow), inset 0 0 0 1px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 236, 190, 0.08);
}
.hero-item-card { flex-direction: column; gap: 7px; padding: 9px 14px 9px 9px; }
.hero-item-main { display: flex; align-items: center; gap: 12px; }
/* Slots (item + skills) are square buttons: icon, keycap in the corner. */
.hero-slot {
  position: relative; flex: none; overflow: hidden;
  width: 62px; height: 62px; padding: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  font-family: "Overused Grotesk", system-ui, sans-serif; color: var(--hud-ink);
  background: radial-gradient(circle at 50% 42%, rgba(66, 92, 70, 0.5), rgba(20, 28, 23, 0.78) 72%);
  border: 1.5px solid rgba(232, 226, 210, 0.32); border-radius: 10px;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.4);
  cursor: pointer; pointer-events: auto; /* inside the click-through HUD wrap */
  user-select: none; touch-action: none;
}
.hero-slot:hover { border-color: rgba(255, 255, 255, 0.55); }
.hero-slot-icon { font-family: var(--hud-emoji); font-size: 1.7rem; line-height: 1; }
.hero-slot-name {
  max-width: 48px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 0.6rem; font-weight: 700;
}
.hero-dock .hero-slot-name { display: none; } /* the tile names it; the touch pad shows it */
.hero-slot-key {
  position: absolute; top: 4px; right: 4px; z-index: 2;
  min-width: 18px; height: 18px; padding: 0 4px;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.66rem; font-weight: 800; line-height: 1;
  background: rgba(14, 13, 11, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.42); border-radius: 5px;
}
.hero-slot-key--mouse { padding: 0; }
.hero-slot-key--mouse svg { width: 9px; height: 11px; }
.hero-slot-key--inspect { padding: 0; cursor: help; pointer-events: auto; }
.hero-slot-key--inspect svg { width: 11px; height: 11px; }
.hero-item-card.is-inspecting .hero-slot-key--inspect { background: var(--dock-gold); color: #1d1a14; border-color: var(--dock-gold); }
.hero-item-text {
  min-width: 0; max-width: clamp(130px, calc(50vw - 400px), 230px);
  display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
}
.hero-item-name {
  /* Rolled names run long ("Swift Ember Gem of the Flock") — two lines, then ellipsis. */
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
  font: 800 1.32rem/1.05 "Fraunces", Georgia, serif; letter-spacing: -0.01em;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
}
/* The type line as a pill: "Common Staff". */
.hero-item-sub {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding: 2px 9px; border-radius: 999px;
  font-size: 0.7rem; font-weight: 600; line-height: 1.3; text-transform: capitalize;
  color: rgba(242, 236, 223, 0.86);
  background: rgba(255, 255, 255, 0.07); border: 1px solid rgba(255, 255, 255, 0.2);
}
/* A carried item's card takes the pointer (the wrap is click-through): on a touch screen the whole
   card is the press-and-hold-to-drop target; a mouse drops with Q only (hero-hud.js). */
.hero-item-card:not(.is-empty) { pointer-events: auto; cursor: default; user-select: none; touch-action: none; }
.hero-item-card.is-empty .hero-slot { border-style: dashed; background: none; cursor: default; }
.hero-item-card.is-empty .hero-slot-key { display: none; }
.hero-item-card.is-empty .hero-item-name { font-size: 0.95rem; color: var(--hud-dim); }
.hero-item-card.is-empty .hero-item-sub { padding: 0; border: 0; background: none; text-transform: none; }
/* Rarity: common white, magical blue, unique gold — on the slot rim and the name. */
.hero-item--common .hero-item-name { color: #f6efe0; }
.hero-item--magical .hero-item { border-color: rgba(125, 184, 255, 0.75); box-shadow: inset 0 0 14px rgba(125, 184, 255, 0.25); }
.hero-item--magical .hero-item-name { color: #7db8ff; }
.hero-item--magical .hero-item-sub { color: #a9cfff; border-color: rgba(125, 184, 255, 0.4); }
.hero-item--unique .hero-item { border-color: rgba(255, 201, 77, 0.8); box-shadow: inset 0 0 14px rgba(255, 201, 77, 0.25); }
.hero-item--unique .hero-item-name { color: #ffc94d; }
.hero-item--unique .hero-item-sub { color: #ffd987; border-color: rgba(255, 201, 77, 0.45); }
/* Hold-to-drop: the slot fills bottom-up over the drop delay (hero-hud.js animates it). */
.hero-item-hold {
  position: absolute; inset: 0;
  background: rgba(255, 255, 255, 0.28);
  transform: scaleY(0); transform-origin: bottom;
  pointer-events: none;
}
/* How strong it is (gameplay.md §10.6): the item's own hit (Shift + click) big — the range
   each hit rolls in — then the bonus lines in blue; a skill tile shows its skill's damage,
   blast and repair the same way, smaller. */
.hero-item-stats, .hero-skill-stats { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; }
.hero-item-stats { margin-top: 2px; }
.hero-item-stats:empty, .hero-skill-stats:empty { display: none; }
.hero-stat { display: inline-flex; align-items: baseline; gap: 5px; white-space: nowrap; }
.hero-stat svg { width: 13px; height: 13px; align-self: center; }
.hero-stat b { font: 800 1.28rem/1 "Fraunces", Georgia, serif; color: #fbe3a1; }
.hero-stat-unit { font-size: 0.74rem; font-weight: 600; color: var(--hud-dim); }
.hero-stat--dmg svg { color: #ff9d3c; }
.hero-stat--blast svg { color: #ffb36b; }
.hero-stat--rep svg { color: #9fe0a8; }
.hero-item-bonuses { display: flex; flex-direction: column; gap: 1px; }
.hero-item-bonuses:empty { display: none; }
.hero-item-bonus { font-size: 0.68rem; font-weight: 600; line-height: 1.25; color: #8fc2ff; }
/* The item's line of flavour along the bottom of the card, behind a gold tick. */
.hero-item-flavor {
  max-width: clamp(200px, calc(50vw - 330px), 310px);
  margin: auto 0 2px 2px; /* the card's foot, however tall the skills card makes it */
  padding: 3px 0 3px 9px;
  border-left: 2px solid rgba(214, 178, 94, 0.6);
  font: italic 500 0.76rem/1.3 "Fraunces", Georgia, serif; letter-spacing: 0.01em;
  color: rgba(242, 236, 223, 0.74);
}
.hero-item--unique .hero-item-flavor { color: rgba(255, 213, 120, 0.9); }
.hero-item-flavor:empty { display: none; }
/* The magnifier's fold-out: how to use what you carry. */
.hero-item-how {
  display: flex; flex-direction: column; gap: 3px;
  padding: 6px 8px; border-radius: 8px;
  background: rgba(0, 0, 0, 0.28); border: 1px solid rgba(255, 255, 255, 0.08);
}
.hero-item-how[hidden] { display: none; }
.hero-item-how-row { display: flex; align-items: center; gap: 4px; font-size: 0.68rem; font-weight: 600; color: var(--hud-dim); }
.hero-item-how-row kbd {
  padding: 1px 5px; border-radius: 4px;
  font: 700 0.62rem/1.4 "Overused Grotesk", system-ui, sans-serif; color: var(--hud-ink);
  background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.28);
}
.hero-item-how-plus { color: var(--hud-dim); font-size: 0.62rem; }
.hero-item-how-what { margin-left: 4px; }
/* Uniques: the slot rim breathes gold and a sheen sweeps across it now and then. */
.hero-item--unique .hero-item { animation: hero-unique-rim 2.8s ease-in-out infinite; }
.hero-item--unique .hero-item::after {
  content: ""; position: absolute; inset: -40%; pointer-events: none;
  background: linear-gradient(115deg, transparent 42%, rgba(255, 226, 150, 0.55) 50%, transparent 58%);
  transform: translateX(-70%); animation: hero-unique-sheen 4.2s ease-in-out infinite;
}
@keyframes hero-unique-rim {
  0%, 100% { box-shadow: inset 0 0 14px rgba(255, 201, 77, 0.25), 0 0 0 0 rgba(255, 201, 77, 0); }
  50% { box-shadow: inset 0 0 18px rgba(255, 201, 77, 0.4), 0 0 10px 1px rgba(255, 201, 77, 0.45); }
}
@keyframes hero-unique-sheen {
  0%, 62% { transform: translateX(-70%); }
  100% { transform: translateX(70%); }
}
@media (prefers-reduced-motion: reduce) {
  .hero-item--unique .hero-item, .hero-item--unique .hero-item::after { animation: none; }
}
/* A drawn icon (icons/skills|items/<id>.png) in place of the emoji. */
.hero-slot-icon.has-img, .hero-passive-icon.has-img { display: inline-flex; }
.hero-slot-icon img { width: 46px; height: 46px; object-fit: contain; pointer-events: none; }
.hero-passive-icon img { width: 14px; height: 14px; object-fit: contain; }
/* Passives the carried item grants (magical/unique): listed under its name. */
.hero-passives { display: flex; flex-wrap: wrap; gap: 2px 8px; margin-top: 1px; }
.hero-passives:empty { display: none; }
.hero-passive { display: inline-flex; align-items: center; gap: 4px; font-size: 0.68rem; font-weight: 600; color: #bfe3b0; }
.hero-passive-icon { font-family: var(--hud-emoji); font-size: 0.8rem; }
/* Skill tiles: the slot, the name, one line on what it does, its numbers. */
.hero-skill-row { gap: 8px; padding: 9px; align-items: stretch; }
.hero-skill-tile {
  width: 124px;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  text-align: center;
}
.hero-skill-tile .hero-slot { margin-bottom: 2px; }
/* The home portal's label over each open portal (hero-portal.js): whose it is and where it leads. */
.hero-portal-tag {
  position: absolute; z-index: 2; transform: translate(-50%, -100%);
  padding: 2px 9px; border-radius: 999px; white-space: nowrap; pointer-events: none;
  font-size: 0.72rem; font-weight: 700; line-height: 1.35; color: #e8f4ff;
  background: rgba(18, 36, 70, 0.74); border: 1px solid rgba(168, 228, 255, 0.55);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
/* Perks (gameplay.md §43): the backpack's slot (what rides in the pack; dashed while empty)
   and the home portal's button, smaller than the item's slot, in a card of their own. */
.hero-perk-row { align-items: center; }
.hero-perk-slot[hidden] { display: none; } /* .hero-slot's display:flex would out-specify [hidden] */
.hero-perk-slot { width: 50px; height: 50px; }
.hero-perk-slot .hero-slot-icon img { width: 36px; height: 36px; }
.hero-perk-slot > .hero-slot-icon svg { width: 26px; height: 26px; }
.hero-pack.is-empty { border-style: dashed; background: none; }
.hero-pack.is-empty .hero-slot-icon { color: rgba(232, 226, 210, 0.45); }
.hero-pack--magical { border-color: rgba(125, 184, 255, 0.75); box-shadow: inset 0 0 10px rgba(125, 184, 255, 0.22); }
.hero-pack--unique { border-color: rgba(255, 201, 77, 0.8); box-shadow: inset 0 0 10px rgba(255, 201, 77, 0.22); }
.hero-portal-icon { color: #8ccaff; }
.hero-portal-btn.is-open { border-color: rgba(140, 202, 255, 0.85); box-shadow: inset 0 0 12px rgba(140, 202, 255, 0.35); }
.hero-portal-btn.is-open .hero-portal-icon { animation: hero-portal-spin 3s linear infinite; }
@keyframes hero-portal-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .hero-portal-btn.is-open .hero-portal-icon { animation: none; } }
.hero-skill-name { font: 800 1.02rem/1.1 "Fraunces", Georgia, serif; color: #f6efe0; text-transform: capitalize; }
.hero-skill-desc {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
  font-size: 0.7rem; font-weight: 500; line-height: 1.25; color: var(--hud-dim);
}
.hero-skill-desc:empty { display: none; }
.hero-skill-stats { justify-content: center; gap: 1px 8px; margin-top: 1px; }
.hero-skill-stats .hero-stat b { font-size: 0.92rem; }
.hero-skill-stats .hero-stat svg { width: 11px; height: 11px; }
.hero-skill-stats .hero-stat-unit { font-size: 0.62rem; }
/* Skills: the cd overlay drains bottom-up after a cast (hero-hud.js animates its scaleY over
   the cooldown); the charge strip fills along the bottom while aiming. */
.hero-skill-cd {
  position: absolute; inset: 0;
  background: rgba(10, 9, 8, 0.74);
  transform: scaleY(0); transform-origin: bottom;
  pointer-events: none;
}
.hero-skill-charge {
  position: absolute; left: 0; right: 0; bottom: 0; height: 4px;
  background: var(--hud-gold);
  transform: scaleX(0); transform-origin: left;
  pointer-events: none;
}
.hero-skill.is-aiming { border-color: rgba(255, 215, 110, 0.85); animation: hero-skill-aim 0.9s ease-in-out infinite; }
@keyframes hero-skill-aim {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 215, 110, 0.35); }
  50% { box-shadow: 0 0 12px 2px rgba(255, 215, 110, 0.5); }
}
.hero-pop { animation: hero-skill-pop 0.5s cubic-bezier(0.2, 1.6, 0.4, 1); }
@keyframes hero-skill-pop {
  0% { transform: scale(0.3); }
  70% { transform: scale(1.15); }
  100% { transform: scale(1); }
}
/* Narrow heroes: the level bar spans the bottom (left of the ♪ chip), the dock sits on top
   of it at the right, the chat on top of it at the left (two fresh lines at most, so the
   bird stays in view), and the item card drops its text (the slot + rim still say it).
   Starts well above tablet widths: with a carried item the dock gets wide, and a bottom
   row squeezed between it and the level bar would cut every phrase short. */
@media (max-width: 1240px) {
  .hero-level { left: 12px; right: 56px; width: auto; }
  .hero-jumps { left: 50%; bottom: 72px; }
  .hero-dock { right: 12px; bottom: 70px; }
  .hero-item-text, .hero-item-flavor, .hero-item-how,
  .hero-skill-name, .hero-skill-desc, .hero-skill-stats { display: none; }
  .hero-skill-tile { width: auto; }
  .hero-dock-cap-rule { display: none; }
  .hero-item-card, .hero-skill-row { padding: 4px; }
  .hero-chatbox {
    left: 12px; bottom: 70px; transform: none; align-items: flex-start;
    width: calc(100% - 190px);
  }
  .hero-chatbox .hero-chat-lines { align-items: flex-start; max-height: 3.4rem; }
  .hero-say-row { justify-content: flex-start; }
  .hero-say { max-width: 10.5rem; font-size: 0.78rem; }
}

/* Hover tooltip over a ground item: its name in the rarity color, tracking the item
   (hero-items.js re-projects it per frame while visible). */
.hero-item-tip {
  position: absolute; z-index: 4;
  transform: translate(-50%, -100%);
  padding: 0.18rem 0.5rem;
  background: rgba(24, 22, 19, 0.82);
  border: 1px solid rgba(255, 255, 255, 0.16); border-radius: 6px;
  font: 700 0.85rem/1.2 "Fraunces", Georgia, serif; color: #f2efe8;
  white-space: nowrap; pointer-events: none;
}
/* Second line: "click to pick up" within reach, "walk closer" beyond it (avatar hosts). */
.hero-item-tip-hint {
  display: block; text-align: center;
  font: 600 0.62rem/1.25 "Overused Grotesk", system-ui, sans-serif; color: rgba(242, 239, 232, 0.72);
}
.hero-item-tip-hint:empty { display: none; }
.hero-item-tip--magical { color: #7db8ff; border-color: rgba(125, 184, 255, 0.45); }
.hero-item-tip--unique { color: #ffc94d; border-color: rgba(255, 201, 77, 0.5); }
/* The card's facts in the tooltip: type line, numbers (▲▼ vs the carried item), bonuses. */
.hero-item-tip { text-align: center; }
.hero-item-tip-name { display: block; }
.hero-item-tip-sub, .hero-item-tip-stats, .hero-item-tip-bonuses {
  display: flex; justify-content: center; flex-wrap: wrap; gap: 0 8px;
  font: 600 0.62rem/1.3 "Overused Grotesk", system-ui, sans-serif;
}
.hero-item-tip-sub { color: rgba(242, 239, 232, 0.62); }
.hero-item-tip-stats { color: #f2efe8; font-weight: 700; }
.hero-item-tip-stats .is-up { color: #8fe39a; font-style: normal; }
.hero-item-tip-stats .is-down { color: #ff8f7a; font-style: normal; }
.hero-item-tip-bonuses { flex-direction: column; color: #8fc2ff; }
.hero-item-tip-sub:empty, .hero-item-tip-stats:empty, .hero-item-tip-bonuses:empty { display: none; }
/* One-time tips (hero-hints.js): a small card over the dock, the gold "Tip" tag and keycaps
   for the keys; fades after a few seconds and never comes back for that player. */
.hero-tip {
  position: absolute; right: 56px; bottom: 170px; z-index: 6;
  max-width: 320px; padding: 8px 12px 8px 9px;
  display: flex; align-items: baseline; gap: 9px;
  background: linear-gradient(180deg, rgba(40, 35, 26, 0.92), rgba(25, 22, 17, 0.9));
  border: 1.5px solid rgba(214, 178, 94, 0.55); border-radius: 10px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28);
  font-family: "Overused Grotesk", system-ui, sans-serif; font-size: 0.8rem; line-height: 1.35;
  color: rgba(255, 252, 244, 0.94); pointer-events: none;
  animation: hero-tip-in 0.35s ease-out both;
  transition: opacity 0.7s ease;
}
.hero-tip.is-fading { opacity: 0; }
.hero-tip-tag {
  flex: none; padding: 1px 6px; border-radius: 6px;
  background: #e2bd66; color: #2a2112;
  font-size: 0.62rem; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase;
}
.hero-tip kbd {
  display: inline-block; padding: 0 5px; margin: 0 1px; border-radius: 4px;
  border: 1px solid rgba(255, 255, 255, 0.35); background: rgba(255, 255, 255, 0.1);
  font: inherit; font-size: 0.74rem; font-weight: 700;
}
[data-touch-ui] .hero-tip { right: 12px; left: 12px; max-width: none; bottom: auto; top: 58px; }
@keyframes hero-tip-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.hero-toast {
  position: absolute; z-index: 5;
  transform: translate(-50%, -100%);
  font-family: "Fraunces", Georgia, serif; font-weight: 700;
  font-size: 1.15rem; color: #ffd76e;
  text-shadow: 0 2px 14px rgba(31, 63, 110, 0.5), 0 0 2px rgba(31, 63, 110, 0.35);
  white-space: nowrap; pointer-events: none;
  animation: hero-emote-rise 2.6s ease-out forwards;
}
.hero-toast--big {
  font-size: 1.8rem; color: #fff;
  animation-duration: 3.4s;
}
/* Level-up: bigger, gold, and it POPS in (springy scale) before the shared rise carries it
   off. The pop must restate the base translate — transform animations replace, not add. */
.hero-toast--level {
  font-size: 2.3rem; color: #ffd76e;
  text-shadow: 0 2px 18px rgba(31, 63, 110, 0.6), 0 0 3px rgba(31, 63, 110, 0.4);
  animation: hero-level-pop 0.55s cubic-bezier(0.2, 1.6, 0.4, 1), hero-emote-rise 3.4s ease-out forwards;
}
@keyframes hero-level-pop {
  0% { transform: translate(-50%, -100%) scale(0); }
  70% { transform: translate(-50%, -100%) scale(1.25); }
  100% { transform: translate(-50%, -100%) scale(1); }
}
.hero-toast--level .hero-letter {
  animation: hero-letter-hop 0.45s cubic-bezier(0.2, 1.8, 0.4, 1) both;
  animation-delay: calc(var(--i) * 40ms);
}
/* Tints for the collect / loot lines: feather tiers + item rarities. */
.hero-tint--black { --tint: #c9b8ff; --tint-deep: #2a1a55; }
.hero-tint--silver { --tint: #f0f5ff; --tint-deep: #3a4556; }
.hero-tint--gold { --tint: #ffd23f; --tint-deep: #6a4300; }
.hero-tint--magical { --tint: #9ccbff; --tint-deep: #12315e; }
.hero-tint--unique { --tint: #ffc94d; --tint-deep: #6a3a00; }
/* A collect's "+3 XP": hops in letter by letter in the tier's color, then the shared rise. */
.hero-toast--xp {
  font-size: 1.45rem; font-weight: 900; color: var(--tint);
  text-shadow: 0 2px 0 var(--tint-deep), 0 0 14px color-mix(in srgb, var(--tint) 70%, transparent);
  animation-duration: 2.2s;
}
.hero-toast--xp .hero-letter {
  animation: hero-letter-hop 0.42s cubic-bezier(0.2, 1.8, 0.4, 1) both;
  animation-delay: calc(var(--i) * 30ms);
}
/* A crate's find: bigger, pops in, then the letters keep a slow wave while it rises. */
.hero-toast--loot {
  font-size: 1.6rem; font-weight: 900; color: var(--tint);
  text-shadow: 0 2px 0 var(--tint-deep), 0 0 18px color-mix(in srgb, var(--tint) 75%, transparent);
  animation: hero-level-pop 0.5s cubic-bezier(0.2, 1.6, 0.4, 1), hero-emote-rise 3.6s ease-out forwards;
}
.hero-toast--loot .hero-letter {
  animation: hero-letter-hop 0.45s cubic-bezier(0.2, 1.8, 0.4, 1) both, hero-letter-wave 1.3s ease-in-out infinite;
  animation-delay: calc(var(--i) * 28ms), calc(0.6s + var(--i) * 55ms);
}
/* Destructible health bars (hero-healthbars.js): projected DOM over a damaged crate —
   appears on first damage, fill ramps green→amber→red, fades out on destruction. */
.hero-hpbar {
  position: absolute; z-index: 4;
  transform: translate(-50%, -100%);
  width: 46px; height: 7px;
  background: rgba(24, 22, 19, 0.75);
  border: 1px solid rgba(255, 255, 255, 0.28); border-radius: 3px;
  overflow: hidden; pointer-events: none;
  transition: opacity 0.25s ease;
}
.hero-hpbar i {
  display: block; height: 100%; border-radius: 2px;
  background: #7ed07e;
  transition: width 0.18s ease, background-color 0.18s ease;
}
/* Trees: the slight variant — a thin leafy line that refills as the crown regrows. */
.hero-hpbar--soft {
  width: 36px; height: 4px;
  background: rgba(24, 40, 36, 0.38);
  border: 0; border-radius: 2px;
}
.hero-hpbar--soft i { border-radius: 2px; transition: width 0.4s ease, background-color 0.4s ease; }
/* Hit numbers (hero-healthbars.js number()): a tiny number rising off what YOU hit — white
   damage, gold (bigger) for a lucky double hit, green "+10" repair work. Pinned to the world
   point; the keyframes do the pop, drift and fade. */
.hero-hitnum {
  position: absolute; z-index: 4;
  transform: translate(-50%, -100%);
  font: 800 0.92rem/1 "Fraunces", Georgia, serif; color: #fff7ea;
  text-shadow: 0 1px 0 rgba(40, 24, 10, 0.9), 0 0 6px rgba(20, 12, 6, 0.55);
  white-space: nowrap; pointer-events: none;
  animation: hero-hitnum-rise 1.1s cubic-bezier(0.2, 0.9, 0.3, 1) forwards;
}
.hero-hitnum--lucky { font-size: 1.22rem; color: #ffd23f; text-shadow: 0 1px 0 #6a4300, 0 0 10px rgba(255, 210, 63, 0.7); }
.hero-hitnum--repair { color: #a8f0b4; text-shadow: 0 1px 0 #17401f, 0 0 6px rgba(20, 60, 30, 0.5); }
.hero-hitnum--spend { color: #ffab93; text-shadow: 0 1px 0 #5a1a0c, 0 0 6px rgba(90, 26, 12, 0.5); } /* "−2 XP" a repair hit cost */
@keyframes hero-hitnum-rise {
  0% { transform: translate(-50%, -100%) scale(0.4); opacity: 0; }
  14% { transform: translate(calc(-50% + var(--dx, 0px) * 0.3), calc(-100% - 8px)) scale(1.18); opacity: 1; }
  30% { transform: translate(calc(-50% + var(--dx, 0px) * 0.5), calc(-100% - 14px)) scale(1); opacity: 1; }
  100% { transform: translate(calc(-50% + var(--dx, 0px)), calc(-100% - 40px)) scale(0.92); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-hitnum { animation: hero-hitnum-fade 1.1s ease-out forwards; }
  @keyframes hero-hitnum-fade { 0%, 60% { opacity: 1; } 100% { opacity: 0; } }
}
/* Repairables (hero-repair.js): "Broken — needs a tool" → "Fixing… 2/3" over a progress bar →
   "Fixed!", projected over the node like the health bars. */
.hero-repair {
  position: absolute; z-index: 4;
  transform: translate(-50%, -100%);
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  pointer-events: none; white-space: nowrap;
  transition: opacity 0.3s ease;
}
.hero-repair-text {
  padding: 3px 9px 4px;
  font-family: "Fraunces", Georgia, serif; font-size: 0.84rem; font-weight: 600;
  color: var(--hud-ink, #f4f0e6);
  background: var(--hud-bg, rgba(24, 22, 19, 0.78));
  border: 1px solid var(--hud-line, rgba(255, 255, 255, 0.22)); border-radius: 6px;
}
.hero-repair .hero-repair-bar { position: static; transform: none; width: 54px; }
.hero-repair .hero-repair-bar i { background: #ffc94d; }
.hero-repair.is-fixed .hero-repair-text { color: #9fe0a0; }
.hero-repair.is-fixed .hero-repair-bar i { background: #7ed07e; }
.hero-repair.is-gone { opacity: 0 !important; }
/* A hit that couldn't pay its XP cost: "Needs 2 XP — find feathers" for a moment. */
.hero-repair.is-denied .hero-repair-text { color: #ffab93; border-color: rgba(255, 138, 106, 0.55); }
/* Vehicles (hero-vehicle.js): "F · hop on" over the tractor in reach ("full" when every seat
   is taken) — projected like the repair labels; touch play uses the pad's Hop on instead. */
.hero-ride {
  position: absolute; z-index: 4;
  transform: translate(-50%, -100%);
  display: flex; align-items: center; gap: 6px;
  padding: 3px 9px 4px;
  font-family: "Fraunces", Georgia, serif; font-size: 0.84rem; font-weight: 600;
  color: var(--hud-ink, #f4f0e6);
  background: var(--hud-bg, rgba(24, 22, 19, 0.78));
  border: 1px solid var(--hud-line, rgba(255, 255, 255, 0.22)); border-radius: 6px;
  pointer-events: none; white-space: nowrap;
  opacity: 0; transition: opacity 0.25s ease;
}
.hero-ride.is-on { opacity: 1; }

/* "connected to server" — a quiet ~2s reassurance the live backend answered (hero-world.js),
   top-right under the online count in light gray, then gone. Never shows on a static host (world stays asleep). */
.hero-connected {
  position: absolute; right: 16px; top: 7.6rem; z-index: 3;
  font-family: var(--mono); font-size: 0.7rem;
  color: rgba(228, 228, 228, 0.75);
  text-shadow: 0 1px 8px rgba(31, 63, 110, 0.45);
  white-space: nowrap; pointer-events: none;
  animation: hero-connected 2.6s ease forwards;
}
@keyframes hero-connected {
  0% { opacity: 0; transform: translateY(6px); }
  12% { opacity: 1; transform: none; }
  72% { opacity: 1; }
  100% { opacity: 0; transform: translateY(-2px); }
}

/* Portal fade (hero-area.js): a white sheet over the whole hero while an area swaps —
   the world melts into the same white the fog/void uses, the mount rebuilds under it. */
.hero-area-fade {
  position: absolute; inset: 0; z-index: 6;
  background: #f6f3ec;
  opacity: 0; pointer-events: none;
  transition: opacity 0.38s ease;
}
.hero-area-fade.is-active { opacity: 1; }
/* A dungeon knockout (hero-game knockOut): a quick white-out while the bird is carried back to
   the level's spawn. Inside the hero slot, over the canvas and under the HUD. */
.hero-ko-fade {
  position: absolute; inset: 0; z-index: 4; pointer-events: none;
  background: radial-gradient(ellipse at center, #fffdf6 0%, #f6efe0 70%, #e9dfc9 100%);
  opacity: 0; transition: opacity 0.32s ease;
}
.hero-ko-fade.on { opacity: 0.92; }
@media (prefers-reduced-motion: reduce) { .hero-ko-fade { transition: none; } }

/* The play hint (hero-boot.js → hero-game.js): the hero's one "this is a game" line, empty
   (hidden) on hosts that get no bird. data-phase="intro": under the title scene's loading
   pill — "a little game, you play the bird". data-phase="live": "You're the bird" and the
   controls (keycaps, or the stick and jump marks on touch) on a HUD card floating over the
   bird — hero-game sets left/top on its head every frame, the tail points down at it — until
   the visitor plays (.is-gone fades it; keep 0.6 s in sync with HINT_FADE_MS). A sibling of
   the slot, z5 and later in the DOM: above the title scene and the meadow's HUD; never takes
   a click, a tap or a swipe. */
.hero-hint {
  position: absolute; left: 50%; top: calc(62% + 3rem); z-index: 5;
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 0.35rem;
  width: max-content; max-width: calc(100% - 24px);
  margin: 0; padding: 0.5rem 0.85rem 0.55rem;
  /* near-opaque: newcomers spawn where other birds stand, and a peer's nametag must not
     show through the words */
  --hint-bg: rgba(22, 20, 17, 0.92);
  background: var(--hint-bg); color: var(--hud-ink);
  border: 1px solid var(--hud-line); border-radius: var(--hud-radius);
  box-shadow: var(--hud-shadow);
  font: 600 0.82rem/1.2 "Overused Grotesk", system-ui, sans-serif;
  text-align: center; text-wrap: balance;
  pointer-events: none;
  animation: hero-hint-in 0.45s cubic-bezier(0.2, 1.3, 0.4, 1) backwards;
  transition: opacity 0.6s ease;
}
.hero-hint:empty { display: none; }
.hero-hint.is-gone { opacity: 0; }
.hero-hint-lead { font-weight: 700; color: var(--hud-gold); }
.hero-hint[data-phase="intro"] .hero-hint-lead::before { content: "\25B6\00a0"; font-size: 0.72em; vertical-align: 0.1em; }
/* Live: its bottom-centre sits on the bird's head (the projected point), the tail below. */
.hero-hint[data-phase="live"] {
  top: 58%;
  transform: translate(-50%, calc(-100% - 17px));
  animation-name: hero-hint-pop;
}
.hero-hint[data-phase="live"]::after {
  content: ""; position: absolute; left: 50%; top: 100%; margin-left: -8px;
  border: 8px solid transparent; border-top-color: var(--hint-bg); border-bottom-width: 0;
}
.hero-hint-keys {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 0.35rem 0.85rem;
}
.hero-hint-group { display: inline-flex; align-items: center; gap: 0.2rem; white-space: nowrap; }
.hero-hint .hero-key { min-width: 1.4rem; height: 1.4rem; font-size: 0.72rem; }
.hero-hint .hero-key--wide { min-width: 2.9rem; }
.hero-hint-mark { padding: 0; border-radius: 50%; color: var(--hud-gold); border-bottom-width: 1px; }
.hero-hint-mark svg { width: 0.95rem; height: 0.95rem; }
.hero-hint-word { margin-left: 0.25rem; }
.hero-hint-data { font-weight: 400; font-size: 0.68rem; opacity: 0.62; letter-spacing: 0.01em; }
.hero-key--mouse svg { width: 0.74rem; height: 0.92rem; }
@keyframes hero-hint-in { from { opacity: 0; transform: translate(-50%, 6px); } }
@keyframes hero-hint-pop { from { opacity: 0; transform: translate(-50%, calc(-100% - 4px)) scale(0.85); } }
@media (max-width: 1240px) {
  /* Before the level bar is earned there's nothing to stack on: chat and dock drop to the
     bottom edge. */
  .hero-scene:has(.hero-level[hidden]) .hero-chatbox,
  .hero-scene:has(.hero-level[hidden]) .hero-dock { bottom: 12px; }
}

/* ---- Touch play (hero-touch.js): phones and tablets steer the bird with a stick bottom-left
   and a pad bottom-right. data-touch-ui on the hero root switches the whole HUD round the
   thumbs: no keycaps, the carried item a lone slot over the pad, the level bar over the stick,
   chat lines above it and the three phrases folding out of the pad's speech button. The
   canvas lets vertical swipes scroll the page (the hero must never be a wall); the stick
   corner and the buttons keep their touches. Coordinates are from the hero's bottom corners;
   --tp-* are the shared offsets. ---- */
.hero-scene[data-touch-ui] {
  --tp-edge: 12px;                                   /* HUD inset from the hero's edges */
  --tp-left: max(var(--tp-edge), env(safe-area-inset-left));
  --tp-right: max(var(--tp-edge), env(safe-area-inset-right));
  --tp-band: 150px;                                  /* the controls' band along the bottom */
}
.hero-scene[data-touch-ui] .hero-canvas { touch-action: pan-y; }
/* Once a control has been used (hero-touch data-touch-play) the canvas keeps its swipes: a thumb
   a little outside the stick used to scroll the page away from under the controls. The ▼ chip
   below scrolls on down; the lock lets go after 20 s idle or once the page has scrolled. */
.hero-scene[data-touch-play] .hero-canvas { touch-action: none; }
.hero-touch {
  position: absolute; inset: 0; z-index: 4;
  pointer-events: none;
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}
.hero-touch[hidden] { display: none; }
/* The stick's corner: a thumb landing anywhere in it brings the stick under it. */
.hero-stick-zone {
  position: absolute; left: 0; bottom: 0;
  width: min(46%, 240px); height: min(52%, 290px); /* 46 %: on a 320 px phone it ends before the Use button */
  pointer-events: auto; touch-action: none;
}
.hero-stick {
  position: absolute; left: calc(var(--tp-left) + 62px); top: calc(100% - 74px);
  width: 112px; height: 112px; margin: -56px 0 0 -56px; border-radius: 50%;
  background: radial-gradient(circle, rgba(22, 20, 17, 0.14) 0 45%, rgba(22, 20, 17, 0.4));
  border: 1.5px solid rgba(255, 255, 255, 0.5);
  box-shadow: var(--hud-shadow);
  opacity: 0.72;
  transition: opacity 0.2s ease, left 0.16s ease, top 0.16s ease;
}
.hero-stick-knob {
  position: absolute; left: 50%; top: 50%;
  width: 50px; height: 50px; margin: -25px 0 0 -25px; border-radius: 50%;
  background: radial-gradient(circle at 38% 32%, #fffdf6, #e6dcc6);
  box-shadow: 0 3px 10px rgba(12, 22, 38, 0.35);
  transition: transform 0.2s cubic-bezier(0.2, 1.4, 0.4, 1);
}
.hero-stick-zone.is-active .hero-stick { opacity: 1; transition: none; }
.hero-stick-zone.is-active .hero-stick-knob { transition: none; }
/* The pad: jump in the corner, the use button beside it, the action button over the jump,
   the speech button over the use button. */
.hero-pad {
  position: absolute; right: var(--tp-right); bottom: var(--tp-edge);
  width: 160px; height: var(--tp-band);
  pointer-events: none;
}
.hero-pad-btn {
  position: absolute; padding: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  font: 700 0.62rem/1 "Overused Grotesk", system-ui, sans-serif; color: var(--hud-ink);
  background: var(--hud-bg);
  border: 1.5px solid rgba(255, 255, 255, 0.34); border-radius: 50%;
  box-shadow: var(--hud-shadow);
  pointer-events: auto; touch-action: none; cursor: pointer;
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.08s ease, background-color 0.08s ease;
}
.hero-pad-btn[hidden] { display: none; }
.hero-pad-btn svg { width: 50%; height: 50%; }
.hero-pad-btn:active, .hero-pad-jump.is-down { transform: scale(0.93); background: var(--hud-bg-strong); }
.hero-pad-jump { right: 0; bottom: 4px; width: 78px; height: 78px; }
.hero-pad-jump svg { width: 46%; height: 46%; margin-bottom: 8px; }
.hero-pad-act { right: 10px; bottom: 92px; width: 58px; height: 58px; color: var(--hud-gold); border-color: rgba(255, 215, 110, 0.7); }
.hero-pad-act svg { width: 24px; height: 24px; }
.hero-pad-act-icon { display: flex; }
.hero-pad-act--magical { color: #7db8ff; border-color: rgba(125, 184, 255, 0.8); }
.hero-pad-act--unique { color: #ffc94d; border-color: rgba(255, 201, 77, 0.9); }
.hero-pad-chat { right: 98px; bottom: 80px; width: 46px; height: 46px; }
.hero-pad-chat[aria-expanded="true"] { background: var(--hud-bg-strong); border-color: var(--hud-gold); color: var(--hud-gold); }
.hero-scene:has(> .hero-chatbox[hidden]) .hero-pad-chat { display: none; } /* no transport, no chat */
/* The use button is the HUD's skill slot (hero-hud setSkillHost), grown round. */
.hero-pad-use { position: absolute; right: 90px; bottom: 0; width: 66px; height: 66px; }
.hero-pad-use .hero-slot {
  position: absolute; inset: 0; width: auto; height: auto;
  border-radius: 50%; border-width: 1.5px;
  background: var(--hud-bg); box-shadow: var(--hud-shadow);
  -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent;
}
.hero-pad-use .hero-slot:active { background: var(--hud-bg-strong); }
.hero-pad-use .hero-slot-name { max-width: 52px; font-size: 0.58rem; }
/* The air-flaps left, as dots along the jump button's foot. */
.hero-scene[data-touch-ui] .hero-jumps {
  left: auto; right: calc(var(--tp-right) + 39px); bottom: calc(var(--tp-edge) + 16px); z-index: 5;
  transform: translateX(50%); gap: 4px;
}
.hero-scene[data-touch-ui] .hero-jump-pip { width: 7px; height: 7px; }
/* HUD round the thumbs: no keycaps, no captions, the item without its text. */
.hero-scene[data-touch-ui] .hero-slot-key,
.hero-scene[data-touch-ui] .hero-key,
.hero-scene[data-touch-ui] .hero-dock-cap,
.hero-scene[data-touch-ui] .hero-item-text,
.hero-scene[data-touch-ui] .hero-item-flavor,
.hero-scene[data-touch-ui] .hero-item-how { display: none; }
.hero-scene[data-touch-ui] .hero-level {
  left: var(--tp-left); right: auto; bottom: calc(var(--tp-edge) + var(--tp-band) - 8px);
  width: min(220px, calc(100% - 2 * var(--tp-edge) - 90px));
  gap: 8px; padding: 4px 10px 5px 4px;
}
.hero-scene[data-touch-ui] .hero-level-badge { width: 32px; height: 32px; }
.hero-scene[data-touch-ui] .hero-level-num { font-size: 1.05rem; }
/* (.hero-hud in the chain: outranks the narrow-hero :has(.hero-level[hidden]) placement) */
.hero-scene[data-touch-ui] .hero-hud .hero-dock { right: var(--tp-right); bottom: calc(var(--tp-edge) + var(--tp-band) + 14px); }
.hero-scene[data-touch-ui] .hero-dock-group--item:has(.hero-item-card.is-empty) { display: none; }
.hero-scene[data-touch-ui] .hero-item-card { padding: 4px; }
/* Chat: the lines stack over the level bar (left), the phrases fold out over the pad. */
.hero-scene[data-touch-ui] .hero-chatbox:not([hidden]) { display: contents; }
.hero-scene[data-touch-ui] .hero-chat-lines {
  position: absolute; left: var(--tp-left); bottom: calc(var(--tp-edge) + var(--tp-band) + 44px); z-index: 3;
  width: min(64%, 300px); max-height: 3.4rem; align-items: flex-start;
  pointer-events: none;
}
.hero-scene[data-touch-ui]:has(.hero-level[hidden]) .hero-chat-lines { bottom: calc(var(--tp-edge) + var(--tp-band) + 6px); }
.hero-scene[data-touch-ui] .hero-say-row { display: none; }
.hero-scene[data-touch-ui].is-chat-open .hero-say-row {
  display: flex; position: absolute; z-index: 5;
  right: calc(var(--tp-right) + 64px); bottom: calc(var(--tp-edge) + var(--tp-band) + 56px); /* clear of the level bar */
}
.hero-scene[data-touch-ui] .hero-says { flex-direction: column; align-items: flex-end; gap: 8px; }
.hero-scene[data-touch-ui] .hero-say {
  max-width: min(15rem, calc(100vw - 110px)); min-height: 44px;
  padding: 0.62rem 0.95rem; font-size: 0.92rem;
  -webkit-tap-highlight-color: transparent;
}
/* The ♪ chip leaves the jump button's corner for the top right; the online count moves over
   beside it. */
.hero-scene[data-touch-ui] .hero-sound-chip { top: 5.2rem; bottom: auto; right: var(--tp-right); }
.hero-scene[data-touch-ui] .hero-keys { left: var(--tp-left); }
.hero-scene[data-touch-ui]:has(> .hero-sound-chip) .hero-online,
.hero-scene[data-touch-ui]:has(> .hero-sound-chip) .hero-online-list { right: calc(var(--tp-right) + 44px); }
/* … and the ⚙ sits under the ♪ (in its place before the ♪ arrives). */
.hero-scene[data-touch-ui] > .hero-gear { top: calc(5.2rem + 44px); bottom: auto; right: var(--tp-right); }
.hero-scene[data-touch-ui]:not(:has(> .hero-sound-chip)) > .hero-gear { top: 5.2rem; }
/* The chips look 34 px but answer to a 44 px square (the step they are stacked at), the ▼ that
   scrolls on past the game under them. */
.hero-scene[data-touch-ui] .hero-sound-chip::after,
.hero-scene[data-touch-ui] > .hero-gear::after,
.hero-scroll-chip::after { content: ""; position: absolute; inset: -5px; }
.hero-scroll-chip {
  position: absolute; right: var(--tp-right, 12px); top: calc(5.2rem + 88px); z-index: 5;
  width: 34px; height: 34px; padding: 0; border-radius: 50%;
  display: none; align-items: center; justify-content: center;
  color: #e8e2d6; background: rgba(24, 22, 19, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.2); cursor: pointer;
  pointer-events: auto; touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
.hero-scroll-chip svg { width: 20px; height: 20px; }
.hero-scene[data-touch-play] .hero-scroll-chip { display: flex; }
.hero-scene[data-touch-ui]:not(:has(> .hero-sound-chip)) .hero-scroll-chip { top: calc(5.2rem + 44px); }
/* A very narrow phone (a 320 px SE): the pad shrinks a little so the stick and the Use button
   keep a thumb's gap between them. */
@media (max-width: 360px) {
  .hero-pad { transform: scale(0.88); transform-origin: 100% 100%; }
}
/* The aim dot on the Use button: where a thumb dragged from it is pointing. */
.hero-aim-knob {
  position: absolute; left: 50%; top: 50%; z-index: 3;
  width: 14px; height: 14px; margin: -7px 0 0 -7px; border-radius: 50%;
  background: var(--hud-gold); box-shadow: 0 0 0 2px rgba(20, 18, 14, 0.7);
  opacity: 0; pointer-events: none; transition: opacity 0.1s ease;
}
.hero-pad-use.is-aim-drag .hero-aim-knob { opacity: 0.95; }
.hero-pad-use.is-aim-drag .hero-slot { border-color: var(--hud-gold); }
/* A landscape phone: the hero is the screen, so the controls are on it from the start — and
   the name steps up and down a size so it doesn't sit across the middle of the meadow. */
@media (pointer: coarse) and (orientation: landscape) and (max-height: 32rem) {
  .hero { height: 100svh; min-height: 18rem; }
  .hero-copy { top: 4.2rem; }
  .hero-name { font-size: clamp(1.8rem, 11vh, 3rem); }
  .hero-subline { margin-top: 0.3rem; font-size: 0.9rem; }
  .hero-location { margin-top: 0.4rem; }
}
/* A phone held upright: the name fills the width under the nav, so the online count moves up
   into the burger's row — its right side is empty on phones unless the German switch sits
   there, in which case the count drops under the name block instead. */
@media (max-width: 40rem) {
  .hero-online { top: 1.1rem; right: 12px; padding: 0.3rem 0.55rem; font-size: 0.7rem; }
  /* the keys chip sits in the burger's row too, just right of it (the online chip takes the right) */
  .hero-keys { top: 1.1rem; left: 4.1rem; gap: 0.28rem; padding: 0.3rem 0.5rem; font-size: 0.7rem; }
  .hero-scene[data-touch-ui] .hero-keys { left: 4.1rem; }
  .hero-online-list { top: 3.2rem; right: 12px; font-size: 0.72rem; }
  .hero-notices { top: 3.4rem; }
  .hero-notice { font-size: 0.74rem; }
  .hero-scene[data-touch-ui] .hero-sound-chip { top: 0.9rem; }
  .hero-scene[data-touch-ui] > .hero-gear { top: calc(0.9rem + 44px); }
  .hero-scene[data-touch-ui]:not(:has(> .hero-sound-chip)) > .hero-gear { top: 0.9rem; }
  .hero-scene[data-touch-ui] .hero-scroll-chip { top: calc(0.9rem + 88px); }
  .hero-scene[data-touch-ui]:not(:has(> .hero-sound-chip)) .hero-scroll-chip { top: calc(0.9rem + 44px); }
  body:has(.site-header .lang-switch) .hero-online,
  body:has(.site-header .lang-switch) .hero-scene[data-touch-ui] .hero-sound-chip { top: 12.5rem; }
  body:has(.site-header .lang-switch) .hero-scene[data-touch-ui] > .hero-gear { top: calc(12.5rem + 44px); }
  body:has(.site-header .lang-switch) .hero-online-list { top: 14.6rem; }
  body:has(.site-header .lang-switch) .hero-scene[data-touch-ui]:not(:has(> .hero-sound-chip)) > .hero-gear { top: 12.5rem; }
  body:has(.site-header .lang-switch) .hero-scene[data-touch-ui] .hero-scroll-chip { top: calc(12.5rem + 88px); }
  body:has(.site-header .lang-switch) .hero-scene[data-touch-ui]:not(:has(> .hero-sound-chip)) .hero-scroll-chip { top: calc(12.5rem + 44px); }
}

/* ---- "Now on the bench" — the project showcase (bench-scene.js) ----
   Copy left, the transparent 3D bench stage center (over a soft ellipse shadow), station
   chip + project caption right, arrows + dots below. Bold serif heading, light sans copy,
   paintbrush-stroke band background. */
/* ---- Text section block — heading + flow text, three layouts ---------------------------
   A simple, configurable prose block for the pages. Deliberate breathing room top and bottom;
   the .text-section-inner grid carries the heading and the flow text as its two children so
   the same markup serves all three arrangements. */
.text-section { padding: 5.5rem 0; }
.text-section-inner {
  display: grid; align-items: start;
  gap: 1.25rem 3.5rem;
}
.text-section-h { margin: 0; }
.text-section-body {
  font-size: 1.1rem; line-height: 1.7; color: rgba(26, 26, 26, 0.75);
}
.text-section-body > :first-child { margin-top: 0; }
.text-section-body > :last-child { margin-bottom: 0; }

/* Centered — heading over flow text, both centered in a narrow measure. */
.text-section--centered { text-align: center; }
.text-section--centered .text-section-inner { max-width: 44rem; margin: 0 auto; }
.text-section--centered .text-section-body { max-width: 36em; margin-left: auto; margin-right: auto; }

/* Split — heading in one column, flow text in the other (heading column a touch narrower). */
.text-section--heading-left .text-section-inner,
.text-section--heading-right .text-section-inner { grid-template-columns: 1fr 1.35fr; }
/* Heading-right: DOM order stays heading-first (reads correctly); only the visual side swaps. */
.text-section--heading-right .text-section-head { order: 2; }
.text-section--heading-right .text-section-body { order: 1; }

@media (max-width: 52rem) {
  .text-section { padding: 3.5rem 0; }
  .text-section--heading-left .text-section-inner,
  .text-section--heading-right .text-section-inner { grid-template-columns: 1fr; }
  .text-section--heading-right .text-section-head { order: 0; }
  .text-section--heading-right .text-section-body { order: 0; }
}

/* Panels — a full-bleed 50/50 of two solid colour fields (--ts-head-bg / --ts-body-bg, set
   inline): heavy display heading in one, the flow text as a stack of weighty lines in the
   other. Near-black ink by default; the --*-dark classes flip a field's ink to white. */
.text-section--panels {
  --ts-head-bg: #d23a2c;
  --ts-body-bg: #efe4cf;
  width: 100vw;
  margin: 6rem 0 6rem calc(50% - 50vw);
  padding: 0;
}
.text-section--panels .text-section-inner {
  grid-template-columns: 1fr 1fr;
  gap: 0;
  align-items: stretch;
}
.text-section--panels .text-section-head,
.text-section--panels .text-section-body {
  padding: clamp(3rem, 7vw, 6.5rem) clamp(1.25rem, 5vw, 5rem);
  color: var(--ink);
  display: flex; flex-direction: column; justify-content: center;
}
.text-section--panels .text-section-head { background: var(--ts-head-bg); }
.text-section--panels .text-section-body { background: var(--ts-body-bg); }
.text-section--head-dark .text-section-head { color: #fff; }
.text-section--body-dark .text-section-body { color: #fff; }
.text-section--panels .text-section-h {
  font-family: "Fraunces", Georgia, serif;
  font-weight: 800;
  font-variation-settings: "opsz" 144;
  font-size: var(--section-h);
  line-height: 0.95;
  letter-spacing: -0.03em;
  color: inherit;
}
.text-section--panels .text-section-body {
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(1.25rem, 2.1vw, 1.9rem);
  font-weight: 650;
  line-height: 1.2;
  letter-spacing: -0.01em;
}
.text-section--panels .text-section-body p { margin: 0; }
.text-section--panels .text-section-body p + p { margin-top: 1.1em; }
/* Wrapped lines break evenly instead of leaving one orphan word ("… in / Vienna"). */
.text-section--panels .text-section-body p { text-wrap: balance; }
.text-section--panels .text-section-body a { color: inherit; }
/* A quiet aside in the flow text (<small>: the thesis and honours under a degree) — much
   smaller, thin, its own block tucked right under the line it belongs to (a <br> inside it
   stacks a second aside line at the same size). A block, not inline-block: an inline-block
   sits in a line box as tall as the big type's, which left a gap above and below it. */
.text-section-body small {
  display: block;
  margin-top: 0.2em;
  font-size: 0.55em;
  font-weight: 300;
  letter-spacing: 0.01em;
  line-height: 1.35;
  opacity: 0.85;
}
.text-section-body small a { text-underline-offset: 0.2em; }
/* tags: small outlined pills under the text (skills, tools). */
.text-section-tags {
  list-style: none; margin: 1.6rem 0 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 0.4rem 0.45rem;
}
.text-section-tags li {
  margin: 0;
  padding: 0.3em 0.8em;
  border: 1px solid color-mix(in srgb, currentColor 35%, transparent);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: clamp(0.78rem, 0.9vw, 0.88rem);
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

/* Panels, 40/60 (headWidth '40'): the heading scaled to its column (two lines for a short
   sentence), the body a touch smaller so a typical highlight line fits on one line. */
.text-section--head-40 .text-section-inner { grid-template-columns: 40fr 60fr; }
.text-section--head-40 .text-section-head { padding-left: clamp(1.25rem, 4.5vw, 4.5rem); padding-right: clamp(1.25rem, 3vw, 3rem); }
.text-section--head-40 .text-section-body { padding-left: clamp(1.25rem, 4.5vw, 4.5rem); padding-right: clamp(1.25rem, 3vw, 3rem); font-size: clamp(1.15rem, 1.8vw, 1.75rem); }
.text-section--head-40 .text-section-h { font-size: var(--section-h); }

/* Panels, narrow head (headWidth '35'): a 35/65 split with the display type scaled to its
   column (two lines for a short sentence), and a quieter body — sized for a CV-style list:
   each item's leading `code` span is its small year column, the **bold** run the role, the
   rest (after a line break) the place. Paragraphs after the list read as smaller notes. */
.text-section--head-35 .text-section-inner { grid-template-columns: 35fr 65fr; }
.text-section--head-35 .text-section-head { padding-left: clamp(1.25rem, 4vw, 4rem); padding-right: clamp(1.25rem, 3vw, 3rem); }
.text-section--head-35 .text-section-h { font-size: var(--section-h); }
.text-section--head-35 .text-section-body {
  font-size: clamp(1.05rem, 1.3vw, 1.25rem);
  font-weight: 500; line-height: 1.35;
}
.text-section--head-35 .text-section-body > * { max-width: 40rem; }
.text-section--head-35 .text-section-body ul { margin: 0; padding: 0; list-style: none; }
.text-section--head-35 .text-section-body li {
  position: relative; margin: 0;
  padding: 0.8em 0 0.8em 7.25rem;
  border-top: 1px solid color-mix(in srgb, currentColor 18%, transparent);
  font-size: 0.8em; font-weight: 400;
}
.text-section--head-35 .text-section-body li:last-child { border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent); }
.text-section--head-35 .text-section-body li strong { display: block; font-size: 1.25em; font-weight: 700; }
.text-section--head-35 .text-section-body li > code:first-child {
  position: absolute; left: 0; top: 1.25em;
  padding: 0; background: none; border: 0; color: inherit;
  font-family: var(--mono); font-size: 0.78rem; font-weight: 600;
  letter-spacing: 0.06em; font-variant-numeric: tabular-nums; white-space: nowrap;
  opacity: 0.6;
}
.text-section--head-35 .text-section-body ul ~ p:not(.text-section-actions) {
  margin-top: 1.4em;
  font-family: var(--mono); font-size: 0.72rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.1em; opacity: 0.7;
}

@media (max-width: 52rem) {
  .text-section--panels { margin: 3.5rem 0 3.5rem calc(50% - 50vw); padding: 0; }
  .text-section--panels .text-section-inner { grid-template-columns: 1fr; }
}
@media (max-width: 36rem) {
  .text-section--head-35 .text-section-body li { padding-left: 0; }
  .text-section--head-35 .text-section-body li > code:first-child { position: static; display: block; margin-bottom: 0.3em; }
}

/* The block's optional button (button + buttonHref): a pill under the body, ink on light
   fields, flipped on dark ones. */
.text-section .text-section-body > .text-section-actions { margin: 2rem 0 0; }
.text-section .text-section-body .text-section-btn {
  display: inline-flex; align-items: center; gap: 0.4em;
  padding: 0.75rem 1.45rem;
  border-radius: 999px;
  background: var(--ink); color: var(--bg);
  font-family: var(--btn-font);
  font-size: 1rem; font-weight: 600; line-height: 1.2; letter-spacing: 0;
  text-decoration: none; text-transform: none;
  transition: transform 0.15s ease, background 0.15s ease;
}
.text-section .text-section-body .text-section-btn:hover { background: #333; transform: translateY(-1px); }
.text-section--body-dark .text-section-body .text-section-btn { background: #fff; color: var(--ink); }
.text-section--body-dark .text-section-body .text-section-btn:hover { background: #f1ede4; }
@media (prefers-reduced-motion: reduce) {
  .text-section .text-section-body .text-section-btn { transition: none; }
  .text-section .text-section-body .text-section-btn:hover { transform: none; }
}

/* Full-bleed band (Georg 2026-07-10): the bench reads as its own room, not floating objects.
   Breakout from .site-main's 60rem column; the inner wrap restores the column width. */
.bench-show {
  width: 100vw; margin-left: calc(50% - 50vw);
  margin-top: 5rem;
  /* Paintbrush strokes under an eggshell veil (keeps the type legible); the flat tone
     paints while the texture streams in. */
  background:
    linear-gradient(rgba(244, 241, 234, 0.72), rgba(244, 241, 234, 0.72)),
    #ece7dc url("assets/textures/paintbrushesBG.jpg") center / cover no-repeat;
  border-top: 1px solid rgba(0, 0, 0, 0.1);
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  padding: 3.5rem 2rem 3rem;
}
.bench-show-inner { max-width: 60rem; margin: 0 auto; }
/* Top to bottom: centered heading → the 3D row bleeding the full viewport width → the
   focused project's caption beside the post's square photo, 70/30. */
.bench-show-h {
  margin: 0;
  text-align: center;
  font-family: var(--mono); /* Overused Grotesk — a poster voice, not the serif headings */
  font-weight: 800;
  font-size: calc(var(--section-h) * 0.92);
  line-height: 0.95;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  text-wrap: balance;
}
.bench-show-intro {
  margin: 0.75rem auto 0; max-width: 36rem; text-align: center;
  font-weight: 350; font-size: 1rem; line-height: 1.55;
  color: rgba(0, 0, 0, 0.62);
}

/* The stage: breaks out of the 60rem column up to 84rem (Georg 2026-09-29: on a full-width
   window the three objects drifted far apart — bench-scene.js spreads them with the canvas
   width, so the cap keeps them together), transparent canvas (models + their contact shadows)
   over the band, the object titles hanging under it. pan-y leaves vertical scrolling to the
   browser; horizontal swipes shuffle. */
.bench-show-stage {
  position: relative;
  width: min(100vw, 84rem); margin: 1rem 0 0 calc(50% - min(50vw, 42rem));
  height: clamp(300px, 28vw, 440px);
  touch-action: pan-y;
}
/* Prev / next arrows at the edges of the text panel below the stage, centered on it (Georg
   2026-09-30; the pixel glyphs of the table carousels). Past 84rem they stay with the stage's
   width instead of hugging the window edge. Under 64rem the panel's text reaches the edges,
   so they move up beside the title (see the media queries). */
.bench-arrow {
  position: absolute; top: 50%; z-index: 2;
  width: 3rem; height: 3rem; margin-top: -1.5rem; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%;
  background: rgba(244, 241, 234, 0.8); color: var(--ink);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
  cursor: pointer;
  transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;
}
.bench-arrow[hidden] { display: none; }
.bench-arrow svg { width: 1.9rem; height: 1.9rem; display: block; }
.bench-arrow--prev { left: max(1rem, 3%, calc(50% - 41rem)); }
.bench-arrow--next { right: max(1rem, 3%, calc(50% - 41rem)); }
.bench-arrow:hover, .bench-arrow:focus-visible { color: var(--accent); background: #fff; transform: scale(1.1); }
.bench-arrow:focus-visible { outline: 2px dashed rgba(0, 0, 0, 0.55); outline-offset: 2px; }
.bench-vignette { position: absolute; inset: 0 0 1.75rem; }
.bench-vignette canvas { display: block; width: 100%; height: 100%; }
/* Invisible hit areas over the two side objects (bench-scene.js fits them to the projected
   slots; these percentages are the no-JS / pre-mount fallback). */
.bench-pick {
  position: absolute; top: 18%; bottom: 30%; width: 22%;
  padding: 0; border: 0; border-radius: 0.75rem;
  background: transparent; cursor: pointer;
}
.bench-pick--left { left: 8%; }
.bench-pick--right { right: 8%; }
.bench-pick:focus-visible { outline: 2px dashed rgba(0, 0, 0, 0.55); outline-offset: 2px; }
/* Over the focused object: click-and-hold spins it. touch-action none so a finger drag
   turns the object instead of scrolling or swiping (fallback box until JS fits it). */
.bench-grab {
  position: absolute; left: 36%; width: 28%; top: 8%; bottom: 22%;
  cursor: grab; touch-action: none;
}
.bench-grab.is-grabbing { cursor: grabbing; }
/* Each object's title under it — bench-scene.js sets transform (position + side shrink) and
   opacity every frame; clicks fall through to the pick buttons. */
.bench-label {
  position: absolute; left: 0; top: 0;
  transform-origin: 50% 0;
  width: max-content; max-width: min(15rem, 27vw); /* long names wrap instead of colliding on phones */
  text-align: center; line-height: 1.15; text-wrap: balance;
  pointer-events: none;
  font-family: var(--mono); font-weight: 600;
  font-size: clamp(0.85rem, 0.7rem + 0.45vw, 1.1rem);
  letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--ink);
}
.bench-label[hidden] { display: none; }
.bench-label.is-focused { font-weight: 800; }

/* Under the stage: the focused project's title + meta, centered below its object; then its
   tagline + link (70) beside the post's square photo (30), in a narrower, vertically centered
   row. Every slide's head, caption and photo sits stacked in its own grid cell — the tallest
   sets the height, so a swap never shifts anything. Only .is-active shows; bench-scene.js
   moves the class and they cross-fade together. visibility keeps the hidden ones out of tab
   order + a11y. */
/* One tinted band under the stage holds the focused project's title, text + photo: it bleeds
   the full viewport width and runs to the section's bottom edge (the section drops its own
   bottom padding); the content keeps a 54rem column. */
.bench-show-info {
  position: relative; /* the prev / next arrows sit at its edges */
  width: 100vw; margin: 0.25rem 0 0 calc(50% - 50vw);
  padding: 2.25rem max(2rem, calc((100vw - 54rem) / 2)) 2.75rem;
  background: rgba(233, 222, 201, 0.9);
}
.bench-show:has(.bench-show-info) { padding-bottom: 0; }
/* nothing in the section is text to select — dragging spins / swipes instead */
.bench-show { -webkit-user-select: none; user-select: none; }
.bench-show-heads { display: grid; text-align: center; }
.bench-show-details { margin-top: 1.5rem; }
.bench-show-details.has-photos {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 3fr);
  gap: 2.5rem;
  align-items: center;
}
.bench-show-captions, .bench-show-photos { display: grid; }
/* the text and its button lean right, onto the photo beside them (Georg 2026-09-29) */
.bench-show-caption { text-align: right; }
.bench-show-caption .bench-show-tagline { margin-left: auto; }
.bench-show-head,
.bench-show-caption,
.bench-show-photo {
  grid-area: 1 / 1;
  opacity: 0; visibility: hidden;
  transition: opacity 0.22s ease, visibility 0s linear 0.22s;
}
.bench-show-head.is-active,
.bench-show-caption.is-active,
.bench-show-photo.is-active {
  opacity: 1; visibility: visible;
  transition: opacity 0.22s ease 0.12s, visibility 0s;
}
.bench-show-title {
  margin: 0 0 0.4rem;
  font-size: clamp(1.75rem, 1.2rem + 1.8vw, 2.5rem);
  line-height: 1.1;
  text-wrap: balance;
}
.bench-show-title a { text-decoration: none; }
.bench-show-title a:hover { text-decoration: none; }
.bench-show-meta {
  margin: 0;
  font-family: var(--mono); font-size: 0.8rem; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: rgba(0, 0, 0, 0.55);
}
/* the category word — the table's colour, a link to its section */
.bench-show-cat { color: var(--st-accent, inherit); font-weight: 800; text-decoration: none; }
.bench-show-cat:hover { text-decoration: underline; text-underline-offset: 3px; }
.bench-show-tagline {
  margin: 0;
  font-size: clamp(1.1rem, 1rem + 0.35vw, 1.3rem); line-height: 1.55;
  color: rgba(0, 0, 0, 0.8);
  text-wrap: pretty;
}
/* the pill button of the text-section panels ("The long version →") */
.bench-show-open {
  display: inline-flex; align-items: center; gap: 0.4em;
  margin-top: 1.1rem;
  padding: 0.75rem 1.45rem;
  border-radius: 999px;
  background: var(--ink); color: var(--bg);
  font-family: var(--btn-font);
  font-size: 1rem; font-weight: 600; line-height: 1.2;
  text-decoration: none;
  transition: transform 0.15s ease, background 0.15s ease;
}
.bench-show-open:hover { background: #333; transform: translateY(-1px); }
.bench-show-photo { display: block; aspect-ratio: 1; }
/* the cover opens the project's images in the lightbox (assets/site.js) */
.bench-photo-open { display: block; height: 100%; cursor: zoom-in; }
.bench-photo-open:focus-visible { outline: 2px dashed rgba(0, 0, 0, 0.55); outline-offset: 3px; }
.bench-show-photo img {
  display: block; width: 100%; height: 100%;
  object-fit: cover;
  border: 1px solid rgba(0, 0, 0, 0.12);
  box-shadow: 3px 4px 0 rgba(0, 0, 0, 0.08);
}
@media (prefers-reduced-motion: reduce) {
  .bench-show-head, .bench-show-caption, .bench-show-photo,
  .bench-show-head.is-active, .bench-show-caption.is-active, .bench-show-photo.is-active { transition: none; }
  .bench-show-open { transition: none; }
  .bench-show-open:hover { transform: none; }
}

@media (max-width: 64rem) {
  /* the text and photo run out to the panel's edges: the arrows flank the title instead,
     and the title keeps clear of them */
  .bench-arrow { top: 2rem; margin-top: 0; }
  .bench-show-heads { padding-inline: 3.4rem; }
}
@media (max-width: 52rem) {
  .bench-show-stage { height: clamp(220px, 58vw, 320px); }
  .bench-arrow { top: 1.6rem; width: 2.4rem; height: 2.4rem; }
  .bench-arrow--prev { left: max(0.6rem, 2%); }
  .bench-arrow--next { right: max(0.6rem, 2%); }
  .bench-arrow svg { width: 1.5rem; height: 1.5rem; }
  .bench-show-heads { padding-inline: 2.4rem; }
  .bench-show-info { padding: 1.75rem 1.25rem 2.25rem; }
  .bench-show-details { margin-top: 1.25rem; }
  .bench-show-details.has-photos { gap: 1.25rem; }
}
@media (max-width: 36rem) {
  .bench-show-details.has-photos { grid-template-columns: 1fr; }
  .bench-show-photos { max-width: 13rem; }
  /* stacked over the photo, the text reads from the left again */
  .bench-show-details.has-photos .bench-show-caption { text-align: left; }
}

.station { margin-top: 8rem; }
/* Divider at the bottom of each station section — a full-width hairline closing the table. */
.station-rule {
  border: 0; border-top: 1px solid rgba(0, 0, 0, 0.22);
  width: 100%; margin: 4rem 0 0;
}
/* Wide screens: the station rows break out of the 56rem text column to 78rem so the 3D
   tables get room and stop clipping (Georg 2026-07-11). The head stays aligned with the
   page's text column; only the workbench grid uses the full width. */
@media (min-width: 64rem) {
  .station {
    width: min(100vw - 4rem, 78rem);
    margin-inline: calc((min(100vw - 4rem, 78rem) - 100%) / -2);
  }
}

/* ---- station head + body (mockup layout, Georg 2026-07-23): centered serif heading,
   centered intro under it (no rule between), then the body pair — table pane left,
   selected-project card right. */
.station-head {
  text-align: center;
  max-width: 44rem;
  margin: 0 auto 3rem;
}
.station-h {
  margin: 0;
  font-size: clamp(2.4rem, 4vw, 3.4rem);
  line-height: 1.05;
  text-wrap: balance;
  /* The station's signature color (stations.js `accent`) — deep enough to read as ink. */
  color: var(--st-accent, var(--ink));
}
/* Optional keyword clause inside the station h2 (stations.js `sub`) — quiet, sub-headline scale. */
.station-intro {
  margin: 1.4rem auto 0;
  max-width: 38rem;
  font-weight: 350; font-size: 0.98rem; line-height: 1.6;
  color: rgba(0, 0, 0, 0.62);
}
/* The body pair sits together on the station's backdrop graphic (Georg 2026-07-23: "behind
   both the bench and the image") — an eggshell veil keeps table + card readable while the
   image sets the station's color theme; the flat tint mix paints while it streams in. The
   background clips to the radius on its own (no overflow:hidden, so the card shadow lives). */
.station-body {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: center;
  padding: clamp(1.25rem, 2.5vw, 2.25rem);
  border-radius: 1.25rem;
  background:
    linear-gradient(rgba(244, 241, 234, 0.62), rgba(244, 241, 234, 0.62)),
    color-mix(in srgb, var(--tint) 28%, #fff) center / cover no-repeat;
}
#station-soldering .station-body { background-image: linear-gradient(rgba(244, 241, 234, 0.62), rgba(244, 241, 234, 0.62)), url("assets/textures/electronicsBG.jpg"); }
#station-fabrication .station-body { background-image: linear-gradient(rgba(244, 241, 234, 0.62), rgba(244, 241, 234, 0.62)), url("assets/textures/fabricationBG.jpg"); }
#station-games .station-body { background-image: linear-gradient(rgba(244, 241, 234, 0.62), rgba(244, 241, 234, 0.62)), url("assets/textures/gameBG.jpg"); }
#station-professional .station-body { background-image: linear-gradient(rgba(244, 241, 234, 0.62), rgba(244, 241, 234, 0.62)), url("assets/textures/websiteBG.jpg"); }

/* ---- the studio map: heading + intro over a row of four station cards ----
   Each card: watercolor table over a torn-paper scrap, label, tagline revealed on
   hover/focus alongside a grow. The paper sways and the table drifts at idle ("breathing"),
   desynced per card. Touch and reduced-motion always show the tagline; reduced motion
   stills the artwork. Papers are 2:3, table drawings ~4:3. */
.studio-map { margin-top: 6rem; }
.studio-map-copy { text-align: center; max-width: 60rem; margin: 0 auto; }
/* the section heading size, and an intro that stays on one line where there's room
   (Georg 2026-09-29: the small heading and the wrapped line looked off) */
.studio-map-copy h2 { font-size: var(--section-h); line-height: 1; margin: 0; }
.studio-map-copy p {
  margin: 1rem 0 0;
  font-weight: 350; font-size: clamp(1.05rem, 0.9rem + 0.4vw, 1.25rem); line-height: 1.5; color: rgba(0, 0, 0, 0.65);
  text-wrap: balance;
}
@media (min-width: 64rem) {
  .studio-map-copy p { white-space: nowrap; }
}
.studio-map-grid {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.25rem;
  margin-top: 2.5rem;
}
/* Wide screens: the card row breaks out of the 56rem text column to 78rem — same breakout
   as the station rows — so the artwork reads at size; the copy stays in the text column. */
@media (min-width: 64rem) {
  .studio-map-grid {
    width: min(100vw - 4rem, 78rem);
    margin-inline: calc((min(100vw - 4rem, 78rem) - 100%) / -2);
    gap: 2rem;
  }
}
.sm-card {
  display: flex; flex-direction: column; align-items: center; gap: 0.2rem;
  text-align: center;
  text-decoration: none; color: var(--ink);
  transform-origin: center bottom;
  transition: transform 0.22s ease;
}
.sm-card:hover, .sm-card:focus-visible { transform: scale(1.06); }
.sm-block {
  position: relative; width: 100%; aspect-ratio: 2 / 3;
  animation: sm-breathe 5.2s ease-in-out infinite alternate;
}
.sm-paper {
  display: block; width: 100%; height: 100%;
  filter: drop-shadow(0 10px 18px rgba(33, 29, 24, 0.1));
}
.sm-table {
  position: absolute; left: 50%; top: 50%; width: 110%;
  transform: translate(-50%, -50%);
  animation: sm-drift 6.8s ease-in-out infinite alternate;
}
/* Desync the idle motion so the four cards never move in lockstep. */
.sm-card:nth-child(2) .sm-block { animation-delay: -1.9s; animation-duration: 5.7s; }
.sm-card:nth-child(3) .sm-block { animation-delay: -3.4s; animation-duration: 4.9s; }
.sm-card:nth-child(4) .sm-block { animation-delay: -0.8s; animation-duration: 5.4s; }
.sm-card:nth-child(2) .sm-table { animation-delay: -2.6s; animation-duration: 7.4s; }
.sm-card:nth-child(3) .sm-table { animation-delay: -4.1s; animation-duration: 6.2s; }
.sm-card:nth-child(4) .sm-table { animation-delay: -1.3s; animation-duration: 7.1s; }
@keyframes sm-breathe {
  from { transform: translateY(0) rotate(0deg); }
  to { transform: translateY(-4px) rotate(0.5deg); }
}
@keyframes sm-drift {
  from { transform: translate(-50%, -50%) rotate(0deg); }
  to { transform: translate(-50%, -53%) rotate(-0.7deg); }
}
.sm-label {
  font-family: var(--mono); font-size: clamp(1.05rem, 0.8rem + 0.7vw, 1.45rem); font-weight: 800;
  text-transform: uppercase; letter-spacing: 0.06em; line-height: 1.15;
  margin-top: 1rem;
  /* The station's signature color, large enough to read as the table's colour. */
  color: var(--st-accent, var(--ink));
}
.sm-sub {
  display: block; min-height: 1.2em;
  font-family: var(--mono);
  font-size: 0.85rem; color: rgba(26, 26, 26, 0.6);
  opacity: 0; transform: translateY(-2px);
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.sm-card:hover .sm-sub, .sm-card:focus-visible .sm-sub { opacity: 1; transform: none; }
@media (hover: none) {
  .sm-sub { opacity: 1; transform: none; }
}
@media (max-width: 52rem) {
  .studio-map-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 40rem) {
  .studio-map { margin-top: 4rem; }
}

/* ---- the station's 3D table pane: transparent over the station-body backdrop ----
   The authored env.camera framing is the shot (workbench-scene.js no longer orbits); the
   canvas renders transparent and floats directly on .station-body's graphic — only the
   ground-shadow ellipse is the pane's own. */
.workbench-stage { position: relative; }
.workbench-3d {
  position: relative;
  height: clamp(360px, 42vw, 520px);
  background: transparent;
}
/* Soft ground shadow under the table — ::before paints above the backdrop but UNDER the
   absolutely-positioned canvas, so the table appears to sit on it. */
.workbench-3d::before {
  content: "";
  position: absolute;
  left: 14%; right: 14%; bottom: 9%; height: 13%;
  background: radial-gradient(ellipse at center, rgba(33, 29, 24, 0.24), transparent 70%);
}
/* Hover lifts, click picks — the JS sets `pointer` while a project is under the mouse. */
.workbench-3d canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.workbench-3d .workbench-ph {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--mono); font-size: 0.75rem; color: rgba(0, 0, 0, 0.35);
}
.workbench-3d[data-workbench-state="live"] .workbench-ph { display: none; }
.workbench-extra { position: absolute; right: 0.75rem; bottom: 0.5rem; z-index: 3; }

/* the lanyard badge (parts.lanyard + assets/lanyard.js): CSS owns the pane size via the
   custom props the partial bakes in; the fallback <img> crossfades out once the rig is live.
   touch-action: none on the canvas — without it touch pointermove dies mid-drag. */
.lanyard-slot {
  position: relative;
  width: 100%;
  max-width: var(--lanyard-maxw, 28rem);
  aspect-ratio: var(--lanyard-aspect, 3 / 4);
  margin-inline: auto;
  overflow: hidden;
}
.lanyard-slot canvas {
  position: absolute; inset: 0; width: 100%; height: 100%; display: block;
  touch-action: none;
  opacity: 0; transition: opacity 0.3s ease;
}
.lanyard-slot[data-lanyard-state="live"] canvas { opacity: 1; }
.lanyard-fallback {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain;
  transition: opacity 0.3s ease;
}
.lanyard-slot[data-lanyard-state="live"] .lanyard-fallback { opacity: 0; }

/* ---- the selected-project panel beside the table (replaced the strip, 2026-07-23) ----
   All cards ship in the DOM (SEO/no-JS: a plain vertical stack). Live ([data-wb-live],
   set by workbench-scene.js) the cards stack into ONE grid cell and only .is-active shows —
   the panel is as tall as the tallest card, so selection changes never shift the layout.
   visibility keeps the hidden cards out of the tab order and away from screen readers. */
.wb-panel { min-width: 0; }
.wb-panel-cards { display: flex; flex-direction: column; gap: 1.5rem; }
[data-wb-live] .wb-panel-cards { display: grid; }
.wb-card { padding: 0; display: flex; flex-direction: column; }
[data-wb-live] .wb-card {
  grid-area: 1 / 1;
  opacity: 0; visibility: hidden;
  transition: opacity 0.35s ease, transform 0.3s ease, visibility 0s linear 0.35s;
}
/* Drag-to-browse (workbench-scene.js writes the inline transform while a card is held):
   pan-y keeps vertical page scrolling alive on touch, grab cursors advertise it, and
   user-select stops a horizontal swipe from smearing a text selection. */
[data-wb-live] .wb-card.is-active {
  opacity: 1; visibility: visible;
  z-index: 1; /* stays on top of the .is-peek card while being dragged */
  transition: opacity 0.35s ease, transform 0.3s ease;
  box-shadow: 0 14px 34px rgba(33, 29, 24, 0.16);
  touch-action: pan-y;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
}
[data-wb-live] .wb-card.is-active:active { cursor: grabbing; }
/* The incoming card while the active one is dragged (workbench-scene.js sets .is-peek and
   drives the scale): shown instantly beneath the held card — the pulled-back drag reveals
   it, so it must not crossfade in. */
[data-wb-live] .wb-card.is-peek {
  opacity: 1; visibility: visible;
  z-index: 0;
  transition: none;
}
.wb-nav { display: flex; align-items: center; justify-content: flex-end; gap: 0.9rem; margin-top: 1.1rem; }
/* Bare pixel arrows (Georg 2026-07-23: no chip, just big glyphs) — inlined icons ride
   currentColor; the generous button box stays as the hit target. */
.wb-arrow {
  width: 2.6rem; height: 2.6rem; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%;
  background: transparent; color: var(--ink);
  cursor: pointer;
  transition: color 0.15s ease, transform 0.15s ease;
}
.wb-arrow svg { width: 2rem; height: 2rem; display: block; }
.wb-arrow:hover, .wb-arrow:focus-visible { color: var(--accent); transform: scale(1.12); }
.wb-counter { font-family: var(--mono); font-size: 0.8rem; font-weight: 700; color: rgba(0, 0, 0, 0.75); }

.wb-card-cover { position: relative; }
.wb-card-cover img, .wb-card-cover .ph { width: 100%; aspect-ratio: 16 / 10; height: auto; object-fit: cover; display: block; }
/* Parked covers (home-sections.js wbCoverImg: data-src until the carousel makes the card a
   neighbour of the active one) stay out of the stacked, not-yet-live layout — no JS, the
   <noscript> twin right after shows the cover instead of an empty frame. */
.wb-panel:not([data-wb-live]) .wb-card-cover img[data-src] { display: none; }
.wb-card-flag {
  position: absolute; top: 0.75rem; left: 0.75rem; z-index: 1;
  padding: 0.3rem 0.75rem;
  background: #fff; border-radius: 1rem;
  font-family: var(--mono); font-size: 0.65rem; font-weight: 600; letter-spacing: 0.05em;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
}
.wb-card-body { padding: 1.1rem 1.2rem 1.35rem; display: flex; flex-direction: column; flex: 1; }
.wb-card-title { margin: 0 0 0.2rem; font-size: 1.3rem; line-height: 1.15; }
.wb-card-year {
  margin: 0 0 0.5rem;
  font-family: var(--mono); font-size: 0.7rem; font-weight: 600;
  letter-spacing: 0.08em; color: rgba(0, 0, 0, 0.45);
}
.wb-card-text {
  margin: 0 0 1rem;
  font-weight: 350; font-size: 0.92rem; line-height: 1.5; color: rgba(0, 0, 0, 0.65);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* "View project" as a painted brushstroke (Georg 2026-07-23): the stroke SVG is a CSS
   mask, so background-color IS the paint — recolorable per state. Text sits inside the
   stroke, in the serif button voice. NOTE: the mask clips focus outlines too, so :focus-visible
   recolors the stroke instead. */
.wb-card-more {
  display: inline-block;
  margin-top: auto; align-self: flex-start; /* buttons align across the row's cards */
  padding: 0.6rem 1.5rem 0.7rem;
  border: 0;
  background: var(--ink);
  -webkit-mask: url("assets/textures/paintbrushButton3.svg") center / 100% 100% no-repeat;
  mask: url("assets/textures/paintbrushButton3.svg") center / 100% 100% no-repeat;
  color: var(--bg);
  font-family: var(--btn-font); font-size: 0.88rem; font-weight: 600;
  text-decoration: none;
  transition: background 0.15s ease, transform 0.18s ease;
}
.wb-card-more:hover, .wb-card-more:focus-visible {
  background: var(--accent); color: #fff;
  transform: scale(1.06) rotate(-1.5deg); /* a painted thing gets a painterly wobble */
}

/* ---- cards & cutouts ---- */
.cutout { border: 1px solid var(--ink); background: #fff; }
.proj-card { display: block; padding: 0.75rem; text-decoration: none; }
.proj-card h3 { margin: 0.5rem 0 0.25rem; font-size: 1.05rem; }
.proj-card .cover, .ph { background: rgba(0, 0, 0, 0.06); min-height: 6rem; }

/* ---- logo loop (reactbits LogoLoop) ----
   One logo sequence is baked server-side (parts.logoLoop) so the band reads without JS
   as a static row; assets/logo-loop.js clones the sequence and scrolls the track.
   Sizing rides the inline --logoloop-height / --logoloop-gap custom properties. */
.logoloop-section { margin: 3rem 0; }
.logoloop {
  position: relative;
  overflow: hidden;
  color: var(--ink); /* placeholder marks draw in currentColor */
  --logoloop-height: 36px;
  --logoloop-gap: 48px;
}
.logoloop-track {
  display: flex;
  width: max-content;
  will-change: transform;
  user-select: none;
}
.logoloop-list {
  display: flex;
  align-items: center;
  margin: 0;
  padding: 0;
  list-style: none;
}
.logoloop-item {
  flex: 0 0 auto;
  margin-right: var(--logoloop-gap);
  line-height: 1;
}
.logoloop-node { display: inline-flex; align-items: center; }
.logoloop-item img,
.logoloop-node svg {
  height: var(--logoloop-height);
  width: auto;
  display: block;
  object-fit: contain;
  -webkit-user-drag: none;
}
.logoloop-link {
  display: inline-flex;
  align-items: center;
  border-radius: 4px;
  opacity: 1;
  transition: opacity 0.2s ease;
}
.logoloop-link:hover { opacity: 0.7; }
/* full width: the band runs edge to edge under the full-bleed panels */
.logoloop-section--bleed { width: 100vw; margin-left: calc(50% - 50vw); }
.logoloop-kicker { text-align: center; margin: 0 0 1.25rem; }
/* picked image logos sit quietly in ink-grey and take their own colours on hover */
.logoloop-item img {
  filter: grayscale(1);
  opacity: 0.75;
  transition: filter 0.25s ease, opacity 0.25s ease;
}
.logoloop-item:hover img { filter: none; opacity: 1; }
.logoloop-link:hover:has(img) { opacity: 1; }
.logoloop-link:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
/* edge fade into the eggshell page ground */
.logoloop-fade::before,
.logoloop-fade::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: clamp(24px, 8%, 120px);
  pointer-events: none;
  z-index: 1;
}
.logoloop-fade::before { left: 0; background: linear-gradient(to right, var(--bg), transparent); }
.logoloop-fade::after { right: 0; background: linear-gradient(to left, var(--bg), transparent); }

/* The logo block standing still (loop off, Georg 2026-09-29: "don't loop the icons and keep
   them small"): one quiet centred row under the kicker, wrapping on a narrow screen. Image
   logos sit in ink-grey and take their own colours on hover, like the loop's; text marks draw
   in currentColor. --logorow-height rides inline from the block's logo height. */
.logoloop-section--still { margin: 4.5rem 0 1rem; }
.logorow {
  --logorow-height: 26px;
  display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
  gap: 1.1rem clamp(1.5rem, 4vw, 2.75rem);
  margin: 0; padding: 0; list-style: none;
  color: var(--ink);
}
.logorow-item { margin: 0; line-height: 1; }
.logorow-node { display: inline-flex; align-items: center; }
.logorow-item img,
.logorow-node svg {
  display: block;
  height: var(--logorow-height);
  width: auto; max-width: 8.5rem;
  object-fit: contain;
  -webkit-user-drag: none;
}
.logorow-node svg { opacity: 0.55; transition: opacity 0.25s ease; }
.logorow-item img {
  filter: grayscale(1);
  opacity: 0.6;
  transition: filter 0.25s ease, opacity 0.25s ease;
}
.logorow-item:hover img { filter: none; opacity: 1; }
.logorow-item:hover .logorow-node svg { opacity: 0.9; }
.logorow-link { display: inline-flex; align-items: center; border-radius: 4px; }
.logorow-link:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* --- /projects/ index (crawlable hub — deliberately plain) --- */
.pi-lede { max-width: 46rem; }
.pi-list { list-style: none; margin: 2rem 0 0; padding: 0; max-width: 46rem; }
.pi-item { margin: 0; border-bottom: 1px solid rgba(0, 0, 0, 0.1); }
.pi-item > a { display: flex; align-items: center; gap: 1rem; padding: 0.6rem 0; text-decoration: none; }
.pi-item > a:hover .pi-name { text-decoration: underline; text-decoration-color: var(--accent); }
.pi-thumb { flex: 0 0 auto; width: 6rem; }
.pi-thumb img, .pi-thumb .ph { width: 6rem; height: 4rem; object-fit: cover; display: block; border: 1px solid var(--ink); min-height: 0; }
.pi-text { display: block; min-width: 0; }
.pi-name { font-weight: 600; }
.pi-year { font-family: var(--mono); font-size: 0.75rem; color: rgba(0, 0, 0, 0.55); margin-left: 0.4rem; }
.pi-line { display: block; font-size: 0.9rem; color: rgba(0, 0, 0, 0.7); }

.post-log { margin-top: 3rem; }
.post-block { margin-top: 2rem; padding-left: 1rem; border-left: 2px solid var(--tint, var(--accent)); }
.post-meta { font-family: var(--mono); font-size: 0.75rem; }
/* .post-title is h3 on project pages, h2 on the Log index, h1 on a permalink page —
   same visual size everywhere except the permalink h1, which keeps page-title scale. */
.post-block h3, .post-block h2.post-title { margin: 0.25rem 0; font-size: 1.35rem; }
.post-title-link { text-decoration: none; color: inherit; }
.post-title-link:hover { text-decoration: underline; }
.post-crumb { font-family: var(--mono); font-size: 0.8rem; margin-top: 1.5rem; }
.repost-chip {
  font-family: var(--mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  padding: 0.05rem 0.4rem;
  border: 1px solid var(--ink);
  text-decoration: none;
  margin-left: 0.4rem;
}
.post-media {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}
.post-media video { grid-column: 1 / -1; width: 100%; }
.post-media .video-embed { grid-column: 1 / -1; }
/* Per-entry "Read more →" on the Log page — a bordered pill that fills on hover, arrow slides. */
.post-more { margin-top: 1rem; }
.read-more {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-family: var(--btn-font);
  font-size: 0.85rem;
  font-weight: 600;
  padding: 0.4rem 0.85rem;
  border: 1px solid var(--ink);
  border-radius: 0.4rem;
  color: var(--ink);
  background: transparent;
  text-decoration: none;
  transition: background-color 0.2s ease, color 0.2s ease;
}
.read-more:hover,
.read-more:focus-visible { background: var(--ink); color: var(--bg); }
.read-more-arrow { transition: transform 0.2s ease; }
.read-more:hover .read-more-arrow,
.read-more:focus-visible .read-more-arrow { transform: translateX(3px); }

/* ---- video (page block + post embeds) ----
   .page-video wraps the pages editor's Video block; .video-embed is the YouTube
   click-to-load facade (parts.js videoFacadeYouTube): a local poster under a real
   <button>, swapped for the youtube-nocookie iframe by assets/video-embed.js. */
.page-video { margin: 2rem 0; }
.page-video video { width: 100%; display: block; }
.page-video figcaption { margin-top: 0.5rem; font-size: 0.85rem; color: rgba(0, 0, 0, 0.55); }

.video-embed {
  position: relative;
  aspect-ratio: 16 / 9;
  width: 100%;
  background: #14120f;
  overflow: hidden;
}
.yt-facade {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.yt-facade img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.yt-facade:focus-visible { outline: 3px solid var(--ink); outline-offset: -3px; }
.yt-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: grid;
  place-items: center;
  width: 4rem;
  height: 4rem;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.65);
  color: #fff;
  transition: background 0.15s ease, transform 0.15s ease;
}
.yt-facade:hover .yt-play, .yt-facade:focus-visible .yt-play {
  background: rgba(0, 0, 0, 0.85);
  transform: translate(-50%, -50%) scale(1.08);
}
.video-embed noscript p { margin: 0; padding: 1rem; }
.video-embed noscript a { color: #fff; }
.video-embed.is-playing iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* Text beside a YouTube facade (parts.js textVideoColumns): posts with an attached video.
   Text is first in DOM; .tv-flip moves the video to the left
   column purely via grid placement, so the narrow-screen stack reads text-first either way.
   align-items:start + sticky keeps a short video in view beside long text. */
.tv-cols {
  display: grid;
  grid-template-columns: 1fr minmax(16rem, 24rem);
  gap: 1.25rem 2rem;
  align-items: start;
}
.tv-media {
  position: sticky;
  top: 1.25rem;
}
.tv-flip { grid-template-columns: minmax(16rem, 24rem) 1fr; }
.tv-flip .tv-text { grid-column: 2; grid-row: 1; }
.tv-flip .tv-media { grid-column: 1; grid-row: 1; }
@media (max-width: 52rem) {
  .tv-cols,
  .tv-flip { grid-template-columns: 1fr; }
  .tv-flip .tv-text,
  .tv-flip .tv-media { grid-column: auto; grid-row: auto; }
  .tv-media { position: static; }
}

/* ---- video feature (the `video-feature` page block, core/site/blocks.js) ----
   One YouTube / Vimeo facade (.video-embed above) in three layouts. --vf-ar (a plain
   number, set inline) shapes the frame, and every frame is capped at 82vh tall, so a 9:16
   Short narrows instead of towering. --vf-bg fills the cinema band / split panel; --dark
   flips the ink to cream on dark picks.
     column — the reading column: kicker / heading / intro over a rounded frame;
     cinema — a full-bleed band with film-strip edges, heading centred over a wide frame;
     split  — a wide panel, the text card beside a 7/12 frame (--video-left / -right). */
.video-feature {
  --vf-ar: 1.7778;
  --vf-ink: var(--ink);
  --vf-muted: rgba(26, 26, 26, 0.56);
  margin: 4.5rem 0;
  color: var(--vf-ink);
}
.video-feature--filled { background: var(--vf-bg); }
.video-feature--dark { --vf-ink: #f4f1ea; --vf-muted: rgba(244, 241, 234, 0.6); }
.video-feature .kicker { color: var(--vf-muted); }
.video-feature-h { color: inherit; }
.video-feature-body { color: color-mix(in srgb, var(--vf-ink) 80%, transparent); }
.video-feature-body > :first-child { margin-top: 0; }
.video-feature-body > :last-child { margin-bottom: 0; }
.video-feature-body a { color: inherit; }
.video-feature-media { margin: 0; }
.video-feature .video-embed {
  aspect-ratio: var(--vf-ar);
  width: min(100%, calc(82vh * var(--vf-ar)));
  margin-inline: auto;
  border-radius: 0.9rem;
  box-shadow: 0 30px 60px -38px rgba(0, 0, 0, 0.45);
}
.video-feature .yt-facade img { transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1); }
.video-feature .yt-facade:hover img { transform: scale(1.03); }
.video-feature .yt-play { width: 4.75rem; height: 4.75rem; }
.video-feature .yt-play svg { width: 42%; height: 42%; }
.video-feature-caption {
  margin-top: 0.85rem;
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--vf-muted);
  text-wrap: pretty;
}

/* column — sits in the 60rem reading column on the page ground. */
.video-feature--column .video-feature-text { max-width: 42rem; margin-bottom: 1.5rem; }
.video-feature--column .video-feature-body { font-size: 1.05rem; }
.video-feature--column.video-feature--tall .video-feature-text,
.video-feature--column.video-feature--tall .video-feature-caption { text-align: center; margin-inline: auto; }

/* cinema — edge to edge, a theatre: big display heading, a wide frame, a cream play disc. */
.video-feature--cinema {
  position: relative;
  width: 100vw;
  margin: 6rem 0 6rem calc(50% - 50vw);
  padding: clamp(3.75rem, 8vw, 6.5rem) clamp(1rem, 4vw, 3rem);
  text-align: center;
}
/* Film-strip sprocket holes along both edges, in the band's own ink. */
.video-feature--cinema::before,
.video-feature--cinema::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: 0.6rem;
  background: color-mix(in srgb, var(--vf-ink) 14%, transparent);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 10'%3E%3Crect x='7' y='0' width='14' height='10' rx='2.5'/%3E%3C/svg%3E") repeat-x 0 0 / auto 100%;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 10'%3E%3Crect x='7' y='0' width='14' height='10' rx='2.5'/%3E%3C/svg%3E") repeat-x 0 0 / auto 100%;
  pointer-events: none;
}
.video-feature--cinema::before { top: 1.1rem; }
.video-feature--cinema::after { bottom: 1.1rem; }
.video-feature--cinema .video-feature-inner { max-width: 72rem; margin: 0 auto; }
.video-feature--cinema .video-feature-text { max-width: 44rem; margin: 0 auto clamp(2rem, 4vw, 3rem); }
.video-feature--cinema .video-feature-h {
  font-variation-settings: "opsz" 144;
  font-size: clamp(2.3rem, 5.5vw, 4.25rem);
  line-height: 0.98;
  letter-spacing: -0.025em;
}
.video-feature--cinema .video-feature-body { font-size: clamp(1.05rem, 1.5vw, 1.2rem); text-wrap: balance; }
.video-feature--cinema .video-embed {
  border-radius: 0.5rem;
  box-shadow: 0 40px 90px -40px rgba(0, 0, 0, 0.7), 0 0 0 1px color-mix(in srgb, var(--vf-ink) 10%, transparent);
}
.video-feature--cinema .yt-play {
  width: clamp(4.75rem, 8vw, 6.5rem);
  height: clamp(4.75rem, 8vw, 6.5rem);
  background: #f4f1ea;
  color: #14120f;
  box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.6);
}
.video-feature--cinema .yt-facade:hover .yt-play,
.video-feature--cinema .yt-facade:focus-visible .yt-play { background: #fff; }
.video-feature--cinema .video-feature-caption {
  margin-top: 1.25rem;
  font-family: var(--mono); font-size: 0.72rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.1em;
}

/* split — a wide panel breaking out of the column; the text card vertically centred
   beside the frame. A blank bg leaves it on the page ground, a colour makes it a card. */
.video-feature--split {
  --vf-w: min(76rem, calc(100vw - 2rem));
  width: var(--vf-w);
  margin: 6rem 0 6rem calc(50% - var(--vf-w) / 2);
}
.video-feature--split.video-feature--filled {
  border-radius: 1.5rem;
  padding: clamp(1.25rem, 4vw, 3.5rem);
  box-shadow: 0 30px 60px -42px rgba(0, 0, 0, 0.4);
}
.video-feature--split .video-feature-inner {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(1.75rem, 4vw, 3.5rem);
  align-items: center;
}
.video-feature--split.video-feature--video-left .video-feature-inner { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); }
.video-feature--video-left .video-feature-text { grid-column: 2; grid-row: 1; }
.video-feature--video-left .video-feature-media { grid-column: 1; grid-row: 1; }
/* A portrait frame takes a narrow column instead of 7/12. */
.video-feature--split.video-feature--tall .video-feature-inner { grid-template-columns: minmax(0, 1fr) minmax(14rem, 22rem); }
.video-feature--split.video-feature--tall.video-feature--video-left .video-feature-inner { grid-template-columns: minmax(14rem, 22rem) minmax(0, 1fr); }
.video-feature--split.video-feature--bare .video-feature-inner { grid-template-columns: minmax(0, 1fr); }
.video-feature--split .video-feature-h {
  font-variation-settings: "opsz" 144;
  font-size: clamp(1.9rem, 3.6vw, 3rem);
  line-height: 1;
  letter-spacing: -0.02em;
}
.video-feature--split .video-feature-body { font-size: 1.05rem; }

@media (max-width: 52rem) {
  .video-feature--cinema { margin: 3.5rem 0 3.5rem calc(50% - 50vw); }
  .video-feature--split { margin-block: 3.5rem; }
  .video-feature--split .video-feature-inner,
  .video-feature--split.video-feature--video-left .video-feature-inner,
  .video-feature--split.video-feature--tall .video-feature-inner,
  .video-feature--split.video-feature--tall.video-feature--video-left .video-feature-inner { grid-template-columns: minmax(0, 1fr); }
  .video-feature--video-left .video-feature-text,
  .video-feature--video-left .video-feature-media { grid-column: auto; grid-row: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .video-feature .yt-facade img { transition: none; }
  .video-feature .yt-facade:hover img { transform: none; }
}

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: 1rem;
  margin-top: 3rem;
}
.ga-item { padding: 0.4rem; }

/* Masonry enhancement (assets/masonry.js): JS sets data-masonry-active and takes over
   layout with absolute items + GSAP transforms. Without JS the grid above still stands.
   height:auto keeps the imgs' width/height attributes as aspect ratio, not fixed size. */
.gallery[data-masonry] .ga-item img { height: auto; }
.gallery[data-masonry-active] {
  display: block;
  position: relative;
}
.gallery[data-masonry-active] .ga-item {
  position: absolute;
  top: 0;
  left: 0;
  will-change: transform;
}
.gallery[data-masonry-active] .ga-item img {
  width: 100%;
  height: auto;
}

.social-proof { margin-top: 3rem; display: grid; gap: 1rem; }
.sp-card { display: block; border: 1px solid var(--ink); padding: 0.75rem; text-decoration: none; }
.sp-platform { font-family: var(--mono); font-size: 0.75rem; text-transform: uppercase; }
.sp-card blockquote { margin: 0.5rem 0 0; }

/* ---- 404 ---- */
.notfound { padding: 4rem 0; max-width: 34rem; }
.notfound h1 { margin: 0.25rem 0 1rem; }
/* The big "404" — Fraunces display size; fuzzy-text.js swaps the text for its canvas.
   Negative margins absorb the canvas's built-in fuzz gutters so it stays left-aligned. */
.nf-code {
  margin: 0 0 0.5rem;
  font-family: "Fraunces", Georgia, serif;
  font-weight: 900;
  font-size: clamp(4.5rem, 16vw, 9rem);
  line-height: 1;
  letter-spacing: -0.02em;
}
.nf-code .fuzzy-text-canvas { display: block; margin-left: -50px; max-width: calc(100% + 50px); }
.notfound-links { margin-top: 2rem; display: flex; gap: 1.5rem; flex-wrap: wrap; }

.visit { margin-top: 2rem; }
.reveal-email { font: inherit; font-family: var(--btn-font); font-weight: 600; padding: 0.5rem 1rem; cursor: pointer; }
.empty { color: rgba(0, 0, 0, 0.45); font-family: var(--mono); font-size: 0.8rem; }

/* ---- feather tally: reusable full-bleed stat band (parts.featherTally) ----
   Breaks out of .site-main like the hero. Each digit is a 1-row window over an
   11-row reel (0–9 + wrap 0); the --d custom property parks the reel on its final
   digit so the number reads right with no JS, and assets/feather-tally.js overrides
   the transform inline to roll it odometer-style on scroll into view. */
.feather-tally {
  --ft-bg: #faf7f0;      /* a lighter eggshell than the page --bg, so the band reads */
  --ft-row: 1.12em;      /* reel row height = the digit window */
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-top: 8rem;
  padding: 4rem 2rem;
  background: var(--ft-bg);
  border-top: 1px solid rgba(26, 26, 26, 0.08);
  border-bottom: 1px solid rgba(26, 26, 26, 0.08);
  text-align: center;
}
.ft-kicker { margin-bottom: 0.75rem; }
.ft-figure {
  margin: 0 0 0.5rem;
  display: flex; align-items: center; justify-content: center; gap: 0.3em;
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(2.8rem, 7vw, 4.6rem);
  font-weight: 800; line-height: 1; letter-spacing: -0.01em;
}
.ft-plume { width: 0.5em; height: 0.5em; color: var(--accent); transform: rotate(-8deg); }
.ft-counter {
  position: relative;
  display: inline-flex;
  font-variant-numeric: tabular-nums;
}
/* soft eggshell fades so the digits appear to roll from under the band */
.ft-counter::before, .ft-counter::after {
  content: ""; position: absolute; left: 0; right: 0; height: 0.22em;
  z-index: 2; pointer-events: none;
}
.ft-counter::before { top: 0; background: linear-gradient(to bottom, var(--ft-bg), transparent); }
.ft-counter::after { bottom: 0; background: linear-gradient(to top, var(--ft-bg), transparent); }
.ft-col {
  position: relative; overflow: hidden;
  width: 1ch; height: var(--ft-row);
}
.ft-reel {
  display: flex; flex-direction: column;
  transform: translateY(calc(var(--d, 0) * var(--ft-row) * -1)); /* rest position, no JS */
  will-change: transform;
}
.ft-reel span {
  height: var(--ft-row); line-height: 1;
  display: flex; align-items: center; justify-content: center;
}
.ft-sep {
  height: var(--ft-row);
  display: flex; align-items: flex-end; padding-bottom: 0.06em; /* sit near the baseline */
}
.ft-label { margin: 0; color: rgba(0, 0, 0, 0.55); }
.ft-sr {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
@media (max-width: 40rem) {
  .feather-tally { margin-top: 4rem; padding: 3rem 1rem; }
}

/* ---- curved loop: reusable curved marquee band (parts.curvedLoop) ----
   One phrase repeated along a shallow SVG arc, full-bleed like the tally band. The
   server bakes enough copies to fill the path so the arc reads without JS;
   assets/curved-loop.js adds the marquee scroll and drag-to-scrub. The default voice
   is the site serif; .cl-* variants swap the font per placement. */
.curved-loop {
  position: relative;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-top: 6rem;
  overflow: hidden;      /* the arc's path bleeds 100 units past both edges */
  touch-action: pan-y;   /* horizontal drag scrubs; the page still scrolls */
}
.curved-loop-svg {
  display: block;
  width: 100%;
  aspect-ratio: 100 / 12;  /* taller than the 12:1 viewBox — headroom for deeper curves */
  overflow: visible;
  font-family: "Fraunces", Georgia, serif;
  font-size: 6rem;         /* resolves in viewBox units: 96 across the 1440-wide path */
  font-weight: 800;
  text-transform: uppercase;
  line-height: 1;
  fill: var(--ink);
  user-select: none;
  -webkit-user-select: none;
}
.cl-sr {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
/* front-page voice: Fraunces italic, lowercase (the "cl-playfair" class name is legacy) */
.curved-loop.cl-playfair .curved-loop-svg {
  font-family: "Fraunces", Georgia, serif;
  font-weight: 700; font-style: italic;
  text-transform: none;
}
@media (max-width: 40rem) {
  .curved-loop { margin-top: 4rem; }
}

/* ---- mobile: the meadow stays playable (touch controls, hero-touch.js), on a hero sized to
   the SMALL viewport (svh) so the collapsing URL bar never resizes the canvas mid-play ---- */
@media (max-width: 40rem) {
  .hero { height: clamp(28rem, 86svh, 50rem); margin-top: -5.5rem; }
  /* …and on a phone that plays it, the whole screen: nothing of the next section peeks out
     under the controls to invite a scroll in the middle of a game. */
  @media (hover: none) and (pointer: coarse) { .hero { height: max(28rem, 100svh); } }
  .site-main { padding: 5.5rem 1rem 1rem; }
  .hero-copy { top: 5.2rem; }
  .station { margin-top: 4rem; }
  .foot-grid { grid-template-columns: 1fr 1fr; gap: 1.5rem; }
  .foot-identity { grid-column: 1 / -1; }
  /* mobile rhythm: these sections carry 6–8rem desktop gaps (measured 112–128px between
     short blocks) that read as dead scroll on a phone — tighten the ones that had no
     mobile step of their own, and reclaim side padding on the full-bleed bands. */
  .contact-cta { margin-top: 4rem; }
  .emoji-comments { margin-top: 4rem; }
  .bench-show { padding: 2.5rem 1.25rem; }
  .site-footer { margin-top: 4rem; padding: 5rem 1.25rem 5rem; }
}

/* ---- touch pointers: grow the small tap controls to a finger-comfortable hit size.
   Gated on (pointer: coarse), not width, so a narrow desktop window keeps the compact
   mouse controls while phones and tablets get the larger targets. ---- */
@media (pointer: coarse) {
  .wb-arrow { width: 2.9rem; height: 2.9rem; }
  .study-btn { padding: 0.65rem 0.9rem; }
  .wb-card-more { padding: 0.75rem 1.7rem 0.85rem; }
  .read-more { padding: 0.55rem 1rem; }
  .lb-close, .lb-nav { min-width: 2.9rem; min-height: 2.9rem; }
}

/* narrow screens: the station body stacks — heading, intro, table pane, then the card. */
@media (max-width: 52rem) {
  .station-body { grid-template-columns: 1fr; gap: 1.25rem; }
  .station-head { margin-bottom: 2rem; }
  .workbench-3d { height: 280px; }
}

@media (prefers-reduced-motion: reduce) {
  .sm-card, .sm-sub { transition: none; }
  .sm-card:hover, .sm-card:focus-visible { transform: none; }
  .sm-sub { opacity: 1; transform: none; }
  .sm-block, .sm-table { animation: none; }
  [data-wb-live] .wb-card { transition: none; }
  .wb-arrow, .wb-card-more { transition: none; }
  .wb-arrow:hover, .wb-arrow:focus-visible,
  .wb-card-more:hover, .wb-card-more:focus-visible { transform: none; }
  .study-cross-ui, .study-cross-ui::after { transition: none; }
  .tsw-char { animation: none; }
  .ec-key { transition: none; }
}

/* ---- study: "How it was made" — a project's work-in-progress assets (core/site/study.js).
   One .study-item figure per asset in authored order: image strips reuse the .gallery
   masonry, code panels reuse .code-typewriter, videos/facades reuse the shared video kit;
   only the 3D viewer and the link-out card are styled here. ---- */
.study { margin-top: 3.5rem; }
.study-item { margin: 1.75rem 0; }
.study-item .code-typewriter { margin: 0; } /* the nested figure — spacing lives on the item */
.study-caption {
  margin-top: 0.5rem;
  font-family: var(--mono);
  font-size: 0.75rem;
  color: rgba(0, 0, 0, 0.55);
}
.study-video video { display: block; width: 100%; border-radius: 0.75rem; }

/* the single-model 3D viewer (assets/study-viewer.js) */
.study-viewer {
  position: relative;
  height: clamp(18rem, 42vw, 28rem);
  border-radius: 1rem;
  overflow: hidden;
  background: #f5f3eb;
  border: 1px solid rgba(0, 0, 0, 0.1);
}
.study-canvas {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
  cursor: grab;
}
.study-canvas:active { cursor: grabbing; }
.study-ph {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--mono); font-size: 0.75rem; color: rgba(0, 0, 0, 0.3);
  pointer-events: none;
}
.study-viewer[data-study-state="live"] .study-ph,
.study-viewer[data-study-state="failed"] .study-ph { display: none; }
.study-fail-note {
  display: none;
  position: absolute; inset: 0;
  align-items: center; justify-content: center;
  margin: 0;
  font-family: var(--mono); font-size: 0.78rem; color: rgba(0, 0, 0, 0.45);
}
.study-viewer[data-study-state="failed"] .study-fail-note { display: flex; }
.study-viewer[data-study-state="failed"] .study-canvas { display: none; }

.study-cross {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.45rem 0.7rem;
  background: rgba(255, 255, 255, 0.82);
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  cursor: pointer;
  user-select: none;
}
/* display:inline-flex above would defeat the hidden attribute — restate it. */
.study-cross[hidden] { display: none; }
.study-cross input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}
.study-cross-ui {
  width: 2rem;
  height: 1.1rem;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.12);
  position: relative;
  transition: background 0.2s ease;
}
.study-cross-ui::after {
  content: "";
  position: absolute;
  top: 0.12rem;
  left: 0.12rem;
  width: 0.86rem;
  height: 0.86rem;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
  transition: transform 0.2s ease;
}
.study-cross input:checked + .study-cross-ui {
  background: #d66a62;
}
.study-cross input:checked + .study-cross-ui::after {
  transform: translateX(0.9rem);
}

.study-toolbar {
  position: absolute;
  left: 0.75rem;
  bottom: 0.75rem;
  display: flex;
  gap: 0.4rem;
}
.study-btn {
  font: inherit;
  font-family: var(--btn-font);
  font-size: 0.8rem;
  padding: 0.45rem 0.75rem;
  border: 1px solid rgba(0, 0, 0, 0.14);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.86);
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.study-btn:hover {
  border-color: var(--ink);
  color: var(--accent);
}
.study-tip {
  position: absolute;
  right: 0.75rem;
  bottom: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.5rem 0.7rem;
  background: rgba(255, 255, 255, 0.82);
  border: 1px solid rgba(0, 0, 0, 0.08);
  border-radius: 0.6rem;
  font-family: var(--mono);
  font-size: 0.65rem;
  color: rgba(0, 0, 0, 0.55);
  pointer-events: none;
}

/* link-out card (GitHub repo etc.) */
.study-link {
  display: inline-flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.85rem 1.1rem;
  text-decoration: none;
  color: inherit;
}
.study-link-label { font-weight: 600; }
.study-link-host {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: rgba(0, 0, 0, 0.55);
}
.study-link:hover .study-link-label { color: var(--accent); }

/* ── Shared 3D perf HUD ────────────────────────────────────────────────
   On-canvas overlay used by the 3D scenes (assets/perf-hud.js). A dark
   translucent chip so it stays legible over any scene — light grass, pale
   sky, cream or blue painterly backdrops alike. Purely decorative and
   pointer-transparent, so it never intercepts a drag. */
.perf-hud {
  position: absolute;
  top: 0.5rem;
  left: 0.5rem;
  z-index: 1;
  padding: 0.4rem 0.55rem;
  border-radius: 0.4rem;
  background: rgba(12, 16, 20, 0.6);
  color: rgba(255, 255, 255, 0.92);
  font-family: var(--mono);
  font-size: 0.66rem;
  line-height: 1.4;
  pointer-events: none;
  user-select: none;
}
.perf-hud-label {
  margin-bottom: 0.2rem;
  font-size: 0.58rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: rgba(255, 255, 255, 0.55);
}
.perf-hud-row {
  display: flex;
  justify-content: space-between;
  gap: 0.85rem;
  min-width: 6rem;
}
.perf-hud-key { color: rgba(255, 255, 255, 0.6); }
.perf-hud-val {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* ---- MediaBetweenText (fancycomponents port; assets/media-between-text.js) -----------
   parts.mediaBetweenText bakes the line OPEN (readable with no JS): two .mbt-text phrases
   around an .mbt-media. The module collapses .mbt-media to width:0 and reveals it on the
   trigger (width transition). is-enhanced marks a block the module has taken over. */
.media-between { margin-top: 4rem; text-align: center; }
/* the bigger picture needs more than the text column to keep the line on one row */
.media-between { width: min(100vw - 2rem, 76rem); margin-left: calc(50% - min(50vw - 1rem, 38rem)); }
/* The kicker in the serif's italic, a real line of its own rather than a tiny caps label
   (Georg 2026-09-29: "say hello needs a different font and size"). */
.media-between .kicker {
  margin-bottom: 1.25rem;
  font-family: "Fraunces", Georgia, serif; font-style: italic;
  font-size: clamp(1.2rem, 1rem + 0.6vw, 1.5rem); font-weight: 500;
  text-transform: none; letter-spacing: 0; color: rgba(0, 0, 0, 0.62);
}
/* Plain (regular-weight) display type and a big picture between the phrases (Georg
   2026-09-29: "make it just normal the text and the image larger"). */
.media-between-text {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.3em;
  margin: 0;
  font-family: "Fraunces", Georgia, serif;
  font-weight: 400;
  font-variation-settings: "opsz" 144;
  letter-spacing: -0.02em;
  line-height: 1.1;
  font-size: clamp(1.8rem, 5vw, 3.6rem);
  text-align: center;
}
.mbt-text { white-space: nowrap; }
.mbt-media {
  display: inline-block;
  vertical-align: middle;
  height: 2.4em;
  border-radius: 14px;
  overflow: hidden;
  flex: none;
}
/* When enhanced, the module drives width inline (0 → measured → auto); before then the media
   sits at its natural width so the baked line reads open. */
.media-between-text.is-enhanced .mbt-media { will-change: width; }
.mbt-media img,
.mbt-media video {
  height: 100%;
  width: auto;
  max-width: none;
  object-fit: cover;
  border-radius: inherit;
}
@media (prefers-reduced-motion: reduce) {
  /* Module leaves the block open and untouched; kill any inline transition just in case. */
  .mbt-media { transition: none !important; }
}

/* ---- text shimmer wave (motion-primitives TextShimmerWave, CSS-only port) -------------
   parts.textShimmerWave bakes one .tsw-char per character carrying --i; the wrapper carries
   --tsw-n (character count) and --tsw-dur (wave travel time). The ripple's period is 2x the
   travel time so the wave rests between passes. Reduced motion: static text (block above). */
.text-shimmer-wave {
  --tsw-base: currentColor;
  --tsw-hi: var(--accent);
  --tsw-dur: 1s;
  display: inline-block;
  position: relative;
}
.tsw-word { display: inline-block; white-space: pre; }
.tsw-char {
  display: inline-block;
  color: var(--tsw-base);
  animation: tsw-wave calc(var(--tsw-dur) * 2) ease-in-out infinite;
  animation-delay: calc(var(--tsw-dur) / var(--tsw-n) * var(--i));
}
@keyframes tsw-wave {
  0%, 12%, 100% { transform: none; color: var(--tsw-base); }
  4% { transform: translateY(-0.32em) scale(1.06) rotate(-4deg); color: var(--tsw-hi); }
  8% { transform: translateY(0.06em) scale(0.99) rotate(2deg); }
}
.tsw-sr {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* ---- letter reveal (animejs ml9 — elastic per-letter scale-in) -------------------------
   parts.letterReveal bakes one .lr-letter per glyph; assets/letter-reveal.js scales them in
   with anime.js. The letters are collapsed (scale 0) ONLY while JS is on and motion is
   allowed, so the module's reveal has nothing to flash before it — a no-JS / reduced-motion
   / anime-missing visitor keeps the baked, full-size heading (the module also forces them
   visible in the anime-missing case). transform-origin bottom = the ml9 "grow up" feel. */
.letter-reveal { position: relative; }
.letter-reveal .lr-text { display: inline-block; }
.letter-reveal .lr-letter {
  display: inline-block;
  transform-origin: 50% 100%;
  line-height: 1em;
  will-change: transform;
}
.letter-reveal .lr-space { display: inline-block; white-space: pre; }
/* Fraunces italic voice (the "cl-playfair" option in the composer), matching the curved loop. */
.letter-reveal.cl-playfair { font-family: "Fraunces", Georgia, serif; font-weight: 700; font-style: italic; }
@media (prefers-reduced-motion: no-preference) {
  html.js .letter-reveal .lr-letter { transform: scale(0); }
}
.lr-sr {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* ---- code typewriter (Typed.js — parts.codeTypewriter) ---------------------------------
   The snippet is baked fully highlighted, so it reads as a plain code panel without JS or
   under reduced motion; assets/code-typewriter.js empties it and types it back in with a
   blinking cursor once the block scrolls into view. Typed.js injects its own .typed-cursor
   blink keyframes — only its look is tuned here. */
.code-typewriter {
  margin: 2rem 0;
  border-radius: 12px;
  background: #1d1f24;
  color: #d7dae2;
  overflow: hidden;
}
.code-typewriter .ct-pre {
  margin: 0;
  padding: 1.1rem 1.3rem;
  overflow-x: auto;
  font-family: var(--mono);
  font-size: 0.85rem;
  line-height: 1.55;
  tab-size: 4;
}
/* Token palette — keyword gold + string ember, after the classic CodePen typewriter. */
.code-typewriter .ct-kw { color: #c0ad60; }
.code-typewriter .ct-str { color: rgba(253, 149, 90, 0.85); }
.code-typewriter .ct-com { color: rgba(215, 218, 226, 0.4); font-style: italic; }
.code-typewriter .ct-num { color: #7fb4a2; }
.code-typewriter .typed-cursor { color: #d7dae2; font-weight: 400; }

/* ---- emoji guestbook (parts.emojiComments; front page, last section before the footer) --
   Visitors compose from the fixed token keyboard — no free text. The composer only works
   with JS (it POSTs), so it hides on html:not(.js); the baked comment list always stands. */
.emoji-comments {
  --ec-border: rgba(0, 0, 0, 0.12);
  --ec-card: #fff;
  margin-top: 6rem;
  max-width: 44rem;
}
.ec-h { margin-top: 0.25rem; }
.ec-composer { margin-top: 1.5rem; }
html:not(.js) .ec-composer { display: none; }
.ec-preview {
  display: block;
  min-height: 2.6rem;
  padding: 0.4rem 0.75rem;
  font-size: 1.4rem;
  line-height: 1.8rem;
  background: var(--ec-card);
  border: 1px solid var(--ec-border);
  border-radius: 0.6rem;
  overflow-wrap: anywhere;
}
.ec-palette {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.75rem;
}
.ec-key {
  min-width: 44px;
  min-height: 44px;
  padding: 0.25rem 0.5rem;
  font-size: 1.3rem;
  background: var(--ec-card);
  border: 1px solid var(--ec-border);
  border-radius: 0.6rem;
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.ec-key:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 10px rgba(26, 26, 26, 0.12);
}
.ec-key:active { transform: translateY(0); }
.ec-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.75rem;
}
.ec-back {
  min-width: 44px;
  min-height: 44px;
  font-size: 1.1rem;
  background: var(--ec-card);
  border: 1px solid var(--ec-border);
  border-radius: 0.6rem;
  cursor: pointer;
}
.ec-send {
  min-height: 44px;
  padding: 0 1.5rem;
  font-family: var(--btn-font);
  font-weight: 600;
  color: #fff;
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 0.6rem;
  cursor: pointer;
}
.ec-send:disabled, .ec-back:disabled, .ec-key:disabled {
  opacity: 0.45;
  cursor: default;
  transform: none;
  box-shadow: none;
}
.ec-status {
  min-height: 1.4rem;
  margin: 0.6rem 0 0;
  font-family: var(--mono);
  font-size: 0.8rem;
  color: rgba(26, 26, 26, 0.65);
}
[data-ec-state="asleep"] .ec-palette { opacity: 0.45; }
.ec-list {
  margin: 1.75rem 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.ec-comment {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.5rem 0.75rem;
  background: var(--ec-card);
  border: 1px solid var(--ec-border);
  border-radius: 0.6rem;
}
.ec-tokens {
  font-size: 1.25rem;
  line-height: 1.7rem;
  overflow-wrap: anywhere;
}
.ec-when {
  flex: none;
  font-family: var(--mono);
  font-size: 0.72rem;
  color: rgba(26, 26, 26, 0.5);
}

/* ---- text highlighter (fancycomponents TextHighlighter, CSS transition port) ----------
   The composer's Highlight button bakes <mark class="text-highlighter"> into the Markdown;
   assets/text-highlighter.js adds .th-active the first time the mark scrolls into view and
   the gradient sweeps left→right via background-size. box-decoration-break clones the fill
   onto every wrapped line. No JS or reduced motion: the highlight simply stands. */
mark.text-highlighter {
  --th-color: hsl(25, 90%, 80%); /* light tint of the site accent */
  color: inherit;
  background: none;
  background-image: linear-gradient(var(--th-color), var(--th-color));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 100%;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  padding: 0.1em 0.15em;
  margin: 0 -0.15em;
  transition: background-size 1s ease-in-out;
}
mark.text-highlighter.th-active,
html:not(.js) mark.text-highlighter {
  background-size: 100% 100%;
}
@media (prefers-reduced-motion: reduce) {
  mark.text-highlighter { transition: none; background-size: 100% 100%; }
}

/* ---- sketch highlights (core/site/highlights.js) + the highlight-list block ---------------
   The sketchbook "background highlighting": a word sits in a hand-drawn hatched box, a
   felt-tip swipe, a loose circle… Each .hl span carries its palette color inline (--hl
   strokes, --hl-soft fills — highlights.js is the one source of colors), so this section
   only holds the STYLES. The marks are pseudo-elements behind the text (.hl-list-inner is
   the stacking context, so they sit under every glyph in the block — neighbours included,
   not just their own word), so the word itself stays plain, selectable type.
   Draw-in: inside a [data-hl-anim] group the strokes stay clipped to nothing until
   assets/text-highlighter.js adds .is-drawn, then wipe in left→right, staggered by --hl-i
   (reading order across the whole block). The gate is html.js-only — no JS, the marks
   simply stand. */
.hl {
  position: relative; display: inline-block;
  padding: 0 0.16em; max-width: 100%;
  /* a highlight rendered without the palette vars still draws, in the site accent */
  --hl: var(--accent); --hl-soft: #f5dcc7;
}
.hl::before,
.hl::after {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  clip-path: inset(0 0 0 0);
  transition: clip-path 0.7s cubic-bezier(0.65, 0, 0.35, 1);
  transition-delay: calc(0.15s + var(--hl-i, 0) * 0.3s);
}
/* the outline lands a beat after the fill — two pen strokes, not one stamp */
.hl::after { transition-delay: calc(0.4s + var(--hl-i, 0) * 0.3s); }
/* Undrawn: clipped from the right. No transition in this state, so dropping .is-drawn (the
   editor's "Replay draw-in") snaps back instantly and the wipe can run again. */
html.js [data-hl-anim]:not(.is-drawn) .hl::before,
html.js [data-hl-anim]:not(.is-drawn) .hl::after {
  clip-path: inset(0 100% 0 0);
  transition: none;
}

/* Hatched box — the sketch: diagonal hatching under a loose, slightly-off outline. The two
   layers are rotated against each other so the box never looks ruled. */
.hl--hatch::before {
  inset: 0.06em 0.02em 0.02em 0.04em;
  background: repeating-linear-gradient(-55deg, var(--hl) 0 0.07em, transparent 0.07em 0.3em);
  opacity: 0.6;
  border-radius: 0.3em 0.1em 0.35em 0.12em;
  transform: rotate(-0.7deg);
}
.hl--hatch::after {
  inset: -0.03em -0.02em -0.01em -0.05em;
  border: 0.07em solid var(--hl);
  border-radius: 0.45em 0.12em 0.4em 0.15em / 0.14em 0.42em 0.12em 0.38em;
  transform: rotate(0.5deg);
}

/* Filled box — the hatched box's loose, slightly-off shape, solidly filled (Georg 2026-09-29:
   "not the striped background but filled"), under the same off-register outline. */
.hl--fill::before {
  inset: 0.06em 0.02em 0.02em 0.04em;
  background: var(--hl);
  opacity: 0.42;
  border-radius: 0.3em 0.1em 0.35em 0.12em;
  transform: rotate(-0.7deg);
}
.hl--fill::after {
  inset: -0.03em -0.02em -0.01em -0.05em;
  border: 0.07em solid var(--hl);
  border-radius: 0.45em 0.12em 0.4em 0.15em / 0.14em 0.42em 0.12em 0.38em;
  transform: rotate(0.5deg);
}

/* Marker — a felt-tip swipe across the lower body of the word (the top stays clear, the way
   a real highlighter misses the ascenders), skewed a touch with rounded ends. */
.hl--marker::before {
  inset: 0.22em -0.06em 0.04em -0.1em;
  background: var(--hl);
  opacity: 0.3;
  border-radius: 0.5em 0.25em 0.45em 0.3em / 0.4em 0.3em 0.5em 0.35em;
  transform: skewX(-10deg) rotate(-1deg);
}

/* Box — the outline alone. */
.hl--box::after {
  inset: -0.02em -0.04em 0;
  border: 0.07em solid var(--hl);
  border-radius: 0.4em 0.14em 0.45em 0.1em / 0.12em 0.4em 0.14em 0.42em;
  transform: rotate(-0.6deg);
}

/* Circle — a loose ellipse drawn around the word, overshooting its box; the side margin keeps
   the ring off the neighbouring words. */
.hl--circle { margin: 0 0.2em; }
.hl--circle::after {
  inset: -0.14em -0.24em -0.1em;
  border: 0.07em solid var(--hl);
  border-radius: 50% 46% 52% 44% / 55% 48% 52% 45%;
  transform: rotate(-2.5deg);
}

/* Underline — a scribbled wave under the word: one wave plus a fainter second pass a little
   lower and out of phase. The wave is a mask (the color stays var(--hl)); mask-size auto
   100% keeps its aspect, so the wavelength scales with the type. */
.hl--underline::before,
.hl--underline::after {
  background: var(--hl);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M0 4C1.5 1.5 4.5 1.5 6 4S10.5 6.5 12 4' fill='none' stroke='%23000' stroke-width='1.7'/%3E%3C/svg%3E") repeat-x 0 50% / auto 100%;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M0 4C1.5 1.5 4.5 1.5 6 4S10.5 6.5 12 4' fill='none' stroke='%23000' stroke-width='1.7'/%3E%3C/svg%3E") repeat-x 0 50% / auto 100%;
}
.hl--underline::before {
  left: 0; right: -0.04em; bottom: -0.14em; height: 0.28em;
  transform: rotate(-0.8deg);
}
.hl--underline::after {
  left: 0.12em; right: 0.2em; bottom: -0.24em; height: 0.24em;
  opacity: 0.5;
  -webkit-mask-position: 0.2em 50%;
  mask-position: 0.2em 50%;
  transform: rotate(0.6deg);
}

/* A highlight in a studio table's colour is a link to that table (highlights.js, the
   stationHref option): the same colour the table wears further down the page. Hover or focus
   and the drawing comes alive — the box swells past the word, the fill deepens, the pen
   goes over the outline again heavier. scale is its own property, so it stacks on each
   style's own rotate() instead of replacing it. The transitions keep the draw-in wipe's
   stagger on clip-path only; the hover moves start at once. */
a.hl-link { color: inherit; text-decoration: none; cursor: pointer; outline: none; }
a.hl-link::before,
a.hl-link::after {
  transition-property: clip-path, scale, opacity, border-width;
  transition-duration: 0.7s, 0.4s, 0.25s, 0.25s;
  transition-timing-function: cubic-bezier(0.65, 0, 0.35, 1), cubic-bezier(0.3, 1.6, 0.5, 1), ease-out, ease-out;
  transition-delay: calc(0.15s + var(--hl-i, 0) * 0.3s), 0s, 0s, 0s;
}
a.hl-link::after { transition-delay: calc(0.4s + var(--hl-i, 0) * 0.3s), 0s, 0s, 0s; }
.hl-list--words a.hl-link::before { transition-delay: calc(0.4s + var(--hl-w, 0) * 0.075s), 0s, 0s, 0s; }
.hl-list--words a.hl-link::after { transition-delay: calc(0.6s + var(--hl-w, 0) * 0.075s), 0s, 0s, 0s; }
a.hl-link:hover::before,
a.hl-link:focus-visible::before { scale: 1.07; opacity: 0.66; }
a.hl-link:hover::after,
a.hl-link:focus-visible::after { scale: 1.09; }
a.hl--fill.hl-link:hover::after,
a.hl--fill.hl-link:focus-visible::after,
a.hl--hatch.hl-link:hover::after,
a.hl--hatch.hl-link:focus-visible::after,
a.hl--box.hl-link:hover::after,
a.hl--box.hl-link:focus-visible::after,
a.hl--circle.hl-link:hover::after,
a.hl--circle.hl-link:focus-visible::after { border-width: 0.1em; }
/* the marker and the wave have no outline to thicken — the marker just fills in */
a.hl--underline.hl-link:hover::before,
a.hl--underline.hl-link:focus-visible::before { opacity: 1; }
a.hl-link:focus-visible { outline: 2px solid var(--hl); outline-offset: 0.12em; border-radius: 0.3em; }
@media (prefers-reduced-motion: reduce) {
  a.hl-link:hover::before, a.hl-link:focus-visible::before,
  a.hl-link:hover::after, a.hl-link:focus-visible::after { scale: none; }
}

/* ---- featured work ----
   A heading on a hairline ink rule, then one row of three flat cards: title → cover → text.
   Each card spans three rows of the grid's subgrid, so titles, covers and texts line up
   across the row even when one title wraps. The title link's ::after covers the whole card
   (block-link pattern): one short link name, the entire card clickable. */
.featured-work { padding: 1.5rem 0 5.5rem; }
.featured-work-head {
  margin: 0 0 2.25rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--ink);
}
.featured-work-h {
  margin: 0;
  font-size: clamp(1.9rem, 3.6vw, 2.6rem);
  line-height: 1;
  letter-spacing: -0.02em;
}
.featured-work-grid {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: auto auto 1fr;
  gap: 0 1.75rem;
}
.fw-card {
  position: relative;
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
  align-content: start;
}
.fw-card-title {
  margin: 0 0 0.85rem;
  font-size: clamp(1.2rem, 1.9vw, 1.4rem);
  font-weight: 700;
  line-height: 1.15;
  align-self: end;
}
.fw-card-link { text-decoration: none; color: inherit; outline: none; }
.fw-card-link::after { content: ""; position: absolute; inset: 0; z-index: 1; }
.fw-card-arrow {
  display: inline-block;
  margin-left: 0.3em;
  color: var(--accent);
  font-weight: 500;
  transition: transform 0.25s ease;
}
.fw-card-media {
  overflow: hidden;
  background: rgba(0, 0, 0, 0.06);
}
.fw-card-media img, .fw-card-media .ph {
  width: 100%; height: auto;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  display: block;
  min-height: 0;
  transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.fw-card-body { padding-top: 1rem; }
.fw-card-meta {
  margin: 0 0 0.35rem;
  font-family: var(--mono); font-size: 0.7rem; font-weight: 600;
  letter-spacing: 0.08em; color: rgba(0, 0, 0, 0.45);
}
.fw-card-text {
  margin: 0;
  font-size: 0.95rem; font-weight: 350; line-height: 1.55;
  color: rgba(0, 0, 0, 0.68);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.fw-card:hover .fw-card-media img { transform: scale(1.04); }
.fw-card:hover .fw-card-arrow { transform: translateX(4px); }
.fw-card:has(.fw-card-link:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 0.6rem;
  border-radius: 2px;
}
@media (max-width: 48rem) {
  .featured-work { padding-bottom: 4rem; }
  .featured-work-grid { grid-template-columns: 1fr; grid-template-rows: none; row-gap: 2.75rem; }
  .fw-card { grid-row: auto; grid-template-rows: none; }
}
@media (prefers-reduced-motion: reduce) {
  .fw-card:hover .fw-card-media img, .fw-card:hover .fw-card-arrow { transform: none; }
}

/* The list block — Georg's sketch: a big lead ("I make"), then dashed lines indented under
   it, the last one optionally left open with a blinking cursor ("more to come"). Items are
   0.6× the lead, so the three sizes (m/l/xl) scale the whole block from one variable. */
.hl-list { padding: 5rem 0; --hl-lead: var(--section-h); }
.hl-list--m { --hl-lead: clamp(2rem, 4.5vw, 3rem); }
.hl-list--xl { --hl-lead: clamp(3rem, 9vw, 6rem); }
.hl-list-kicker { margin-bottom: 1rem; }
/* The block is the stacking context, not each .hl: a span-level context paints its marks in
   the positioned layer, OVER the plain words around it (a circle's arc across the line above,
   an underline reaching the next line). Here every z-index:-1 mark lands under all the
   block's text — and still above a backdrop's fill and sketch strokes. Nothing between .hl
   and this element may get transform/opacity/filter/isolation, or the marks get trapped again. */
.hl-list-inner { position: relative; z-index: 0; }
.hl-list-lead {
  margin: 0;
  font-size: var(--hl-lead);
  line-height: 1.05;
  letter-spacing: -0.02em;
  /* a long German compound at xl on a 360px phone must wrap, not push the page sideways */
  overflow-wrap: break-word;
}
/* The lead's tight 1.05 line-height makes .hl a shorter box that sits higher than the glyphs
   (Fraunces descends to -0.245em; the box stops at -0.165em), so outlines cut through g/p/y
   and leave empty hatching above lowercase words. Move the marks down by about the 0.1em the
   items' 1.25 line-height gives them — shifted, not stretched, so a wrapped lead's lines don't
   collide (the hatch/box top inset must stay ≤ 0.05em to clear ascenders). The marker is left
   alone: ending mid-descender is what a felt-tip does. */
.hl-list-lead .hl--hatch::before,
.hl-list-lead .hl--fill::before { top: 0.14em; bottom: -0.1em; }
.hl-list-lead .hl--hatch::after,
.hl-list-lead .hl--fill::after { top: 0.05em; bottom: -0.12em; }
.hl-list-lead .hl--box::after { top: 0.05em; bottom: -0.11em; }
.hl-list-lead .hl--circle::after { top: -0.06em; bottom: -0.2em; }
.hl-list-lead .hl--underline::before { bottom: -0.24em; }
.hl-list-lead .hl--underline::after { bottom: -0.34em; }
.hl-list-items {
  list-style: none;
  margin: 0.3em 0 0;
  /* stacked: the dashes hang under the lead's second word, like the sketch */
  padding: 0 0 0 1.4em;
  font-family: "Fraunces", Georgia, serif;
  font-weight: 600;
  font-size: calc(var(--hl-lead) * 0.6);
  line-height: 1.25;
  letter-spacing: -0.01em;
}
.hl-list-item {
  display: flex; align-items: baseline; gap: 0.45em;
  padding: 0.14em 0;
}
.hl-list-dash { flex: none; color: var(--ink); font-weight: 500; }
.hl-list-text { min-width: 0; overflow-wrap: break-word; }
/* The open end: an empty line with a cursor. The caret's bottom edge sits on the baseline
   (an empty inline-block's baseline is its bottom), nudged down to cap-height proportions.
   A few blinks, then it stands: WCAG 2.2.2 wants blinking to stop within 5 s (4 × 1.1s; no
   fill-mode, so it ends on the visible base state, not the hidden half of the last cycle). */
.hl-list-caret {
  display: inline-block;
  width: 0.07em; min-width: 2px; height: 0.95em;
  background: currentColor;
  transform: translateY(0.14em);
  animation: hl-caret 1.1s steps(1) 4;
}
@keyframes hl-caret { 50% { opacity: 0; } }
/* The block usually sits below the fold, so the load-time blinks run unseen: blink again when
   the group draws in. A NEW name is what restarts a finished animation (the same name or a
   changed count would not); the base rule stays for blocks that never get .is-drawn. */
html.js [data-hl-anim].is-drawn .hl-list-caret { animation-name: hl-caret-in; }
@keyframes hl-caret-in { 50% { opacity: 0; } }

/* Words rise in (the block's `words` prop): every word — each dash counts as its line's first —
   comes up out of a soft blur, one after another in reading order (--w, set per word by
   highlights.js), and a highlight's strokes wait for their own word (--hl-w) instead of the
   block-wide highlight count. Same gate as the strokes: undrawn = hidden, no transition (so
   the editor's "Replay draw-in" snaps back), .is-drawn = the staggered rise. inline-block,
   since transforms don't apply to plain inline boxes; the spaces between words stay text. */
.hl-list--words .hl-w { display: inline-block; max-width: 100%; }
html.js .hl-list--words[data-hl-anim] .hl-w {
  transition: opacity 0.55s ease-out, transform 0.8s cubic-bezier(0.2, 0.75, 0.15, 1), filter 0.6s ease-out;
  transition-delay: calc(0.1s + var(--w, 0) * 0.075s);
}
html.js .hl-list--words[data-hl-anim]:not(.is-drawn) .hl-w {
  opacity: 0;
  transform: translateY(0.55em) rotate(2.5deg);
  filter: blur(6px);
  transition: none;
}
.hl-list--words .hl::before { transition-delay: calc(0.4s + var(--hl-w, 0) * 0.075s); }
.hl-list--words .hl::after { transition-delay: calc(0.6s + var(--hl-w, 0) * 0.075s); }

/* Side by side: lead left (up to about half the width), list right, first baselines lined up
   so "I make" reads straight into the first line. */
.hl-list--side .hl-list-inner {
  display: grid;
  grid-template-columns: fit-content(55%) minmax(0, 1fr);
  column-gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: baseline;
}
.hl-list--side .hl-list-kicker { grid-column: 1 / -1; }
.hl-list--side .hl-list-lead { grid-column: 1; }
.hl-list--side .hl-list-items { grid-column: 2; margin-top: 0; padding-left: 0; }

/* Backdrops — the whole block on a highlighted ground, in the block's color (--hlb strokes,
   --hlb-soft fills, set inline from the same palette). */
.hl-list--bg {
  position: relative; isolation: isolate;
  /* block-axis only — the bleed band owns margin-left */
  margin-top: 3rem; margin-bottom: 3rem;
  padding: clamp(2.5rem, 6vw, 4.5rem) clamp(1.25rem, 5vw, 3.5rem);
  border-radius: 1.25rem;
}
.hl-list--bg-tint { background: var(--hlb-soft); }
.hl-list--bg-hatch {
  background: repeating-linear-gradient(-45deg, var(--hlb-soft) 0 0.2rem, transparent 0.2rem 0.75rem);
}
/* Sketch box — no fill, a hand-drawn outline plus a fainter second pass off-register, the
   way a pen goes round twice. */
.hl-list--bg-sketch::before,
.hl-list--bg-sketch::after {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  border: 2px solid var(--hlb);
}
.hl-list--bg-sketch::after {
  inset: 0;
  border-radius: 1.6rem 0.9rem 1.8rem 1rem / 1rem 1.7rem 0.9rem 1.5rem;
  transform: rotate(-0.4deg);
}
.hl-list--bg-sketch::before {
  inset: 0.3rem 0 -0.25rem 0.2rem;
  border-width: 1.5px;
  border-radius: 1rem 1.9rem 1.1rem 1.6rem / 1.6rem 0.9rem 1.7rem 1rem;
  opacity: 0.45;
  transform: rotate(0.7deg);
}
/* Full-width band: breaks out of .site-main's column like .bench-show (html's overflow-x:clip
   swallows the scrollbar-width overshoot of 100vw); the inner re-centres to the column. */
.hl-list--bleed {
  width: 100vw; margin-left: calc(50% - 50vw);
  border-radius: 0;
}
.hl-list--bleed .hl-list-inner { max-width: 56rem; margin: 0 auto; }
/* A sketched band keeps only its top and bottom strokes — side strokes would sit on the
   viewport edges — and no tilt: on a 100vw band the rotation's end-drift scales with the
   viewport and the two passes scissor apart into an X. The off-register second pass comes
   from ::before's insets alone. */
.hl-list--bleed.hl-list--bg-sketch::before,
.hl-list--bleed.hl-list--bg-sketch::after {
  left: 0; right: 0;
  border-left: 0; border-right: 0; border-radius: 0;
  transform: none;
}

@media (max-width: 40rem) {
  /* :not() — a backdrop keeps its own (clamped) padding, only the bare block tightens */
  .hl-list:not(.hl-list--bg) { padding: 3rem 0; }
  .hl-list--bg { margin-top: 2rem; margin-bottom: 2rem; }
  .hl-list-items { padding-left: 0.6em; }
  .hl-list--side .hl-list-inner { display: block; }
  .hl-list--side .hl-list-items { margin-top: 0.3em; padding-left: 0.6em; }
}

@media (prefers-reduced-motion: reduce) {
  .hl::before, .hl::after { transition: none; }
  html.js [data-hl-anim]:not(.is-drawn) .hl::before,
  html.js [data-hl-anim]:not(.is-drawn) .hl::after { clip-path: inset(0 0 0 0); }
  html.js .hl-list--words[data-hl-anim] .hl-w,
  html.js .hl-list--words[data-hl-anim]:not(.is-drawn) .hl-w { opacity: 1; transform: none; filter: none; transition: none; }
  .hl-list-caret,
  html.js [data-hl-anim].is-drawn .hl-list-caret { animation: none; }
}

/* Printing fires no IntersectionObserver, so a block never scrolled to would print with its
   strokes still clipped away (and the caret maybe mid-blink): paper gets the finished drawing.
   Same selectors as the gate, so this wins on specificity AND order. */
@media print {
  .hl::before, .hl::after { transition: none; }
  html.js [data-hl-anim]:not(.is-drawn) .hl::before,
  html.js [data-hl-anim]:not(.is-drawn) .hl::after { clip-path: inset(0 0 0 0); }
  html.js .hl-list--words[data-hl-anim] .hl-w,
  html.js .hl-list--words[data-hl-anim]:not(.is-drawn) .hl-w { opacity: 1; transform: none; filter: none; transition: none; }
  .hl-list-caret,
  html.js [data-hl-anim].is-drawn .hl-list-caret { animation: none; }
}

/* --- page blocks (the block-based pages system; core/site/blocks.js) ---------------------- */
/* Single image block. */
.page-image { margin: 2rem 0; }
.page-image img { display: block; max-width: 100%; height: auto; border-radius: 4px; }
.page-image figcaption { margin-top: 0.5rem; font-size: 0.85rem; color: rgba(0, 0, 0, 0.55); }
/* Divider / spacer blocks. */
.page-divider { border: none; border-top: 1px solid rgba(0, 0, 0, 0.12); margin: 3rem 0; }
.page-spacer { width: 100%; }
/* Link button block: a small outlined pill on a line of its own ("All projects →" under the
   last station table on home); it fills with ink on hover. */
.link-button { margin: 2.5rem 0; text-align: center; }
.link-button--left { text-align: left; }
.link-button--right { text-align: right; }
.link-button-a {
  display: inline-flex; align-items: center; gap: 0.4em;
  padding: 0.5rem 1.15rem;
  border: 1.5px solid var(--ink); border-radius: 999px;
  background: transparent; color: var(--ink);
  font-family: var(--btn-font);
  font-size: 0.92rem; font-weight: 600; line-height: 1.2;
  text-decoration: none;
  transition: background 0.15s ease, color 0.15s ease, transform 0.15s ease;
}
.link-button-a:hover, .link-button-a:focus-visible { background: var(--ink); color: var(--bg); transform: translateY(-1px); }
@media (prefers-reduced-motion: reduce) {
  .link-button-a { transition: none; }
  .link-button-a:hover, .link-button-a:focus-visible { transform: none; }
}
/* Standalone vignette embed (assets/vignette-embed.js) — transparent scene over the page. */
.vignette-embed-section { margin: 2rem 0; }
.vignette-embed { position: relative; height: var(--ve-height, clamp(16rem, 40vh, 24rem)); }
.vignette-embed canvas { display: block; width: 100%; height: 100%; }

/* ---- lightbox (site.js) -----------------------------------------------------------------
   Any [data-lightbox] trigger opens the full image over a darkened page — image files are
   never linked directly. The figure is a paper cutout floating on near-black ink; caption
   and counter speak in the mono spec-line voice. */
[data-lightbox] { cursor: zoom-in; }
body.lightbox-open { overflow: hidden; }
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(0.5rem, 2vw, 1.25rem);
  padding: clamp(1rem, 4vw, 3rem);
  background: rgba(20, 18, 15, 0.86);
  opacity: 0;
  transition: opacity 0.2s ease;
}
/* the class rule above out-specifies the UA [hidden]{display:none} — restate it, or the
   closed overlay keeps covering the page invisibly and swallows every click */
.lightbox[hidden] { display: none; }
.lightbox.lb-open { opacity: 1; }
.lb-figure {
  margin: 0;
  padding: 0.6rem;
  max-width: min(92vw, 72rem);
  transform: scale(0.97);
  transition: transform 0.25s ease;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
}
.lightbox.lb-open .lb-figure { transform: scale(1); }
.lb-img {
  display: block;
  max-width: 100%;
  max-height: min(82vh, 80svh);
  width: auto;
  height: auto;
}
.lb-caption {
  margin: 0.5rem 0.1rem 0;
  font-family: var(--mono);
  font-size: 0.75rem;
  color: rgba(26, 26, 26, 0.65);
}
/* credits (core/media-rights.js): under the lightbox caption, under a project's header image */
.lb-credit { display: block; margin-top: 0.3rem; font-size: 0.72rem; opacity: 0.75; }
.media-credit {
  margin: 0.4rem 0 0; text-align: right;
  font-family: var(--mono); font-size: 0.68rem; letter-spacing: 0.02em; color: rgba(0, 0, 0, 0.55);
}
.lb-caption:empty { display: none; }
.lb-close, .lb-nav {
  min-width: 2.6rem;
  min-height: 2.6rem;
  padding: 0;
  font: inherit;
  font-size: 1.4rem;
  line-height: 1;
  color: var(--ink);
  border: 1px solid var(--ink);
  background: var(--bg);
  cursor: pointer;
  transition: background 0.15s ease;
}
.lb-close:hover, .lb-nav:hover { background: #fff; }
.lb-close {
  position: absolute;
  top: clamp(0.75rem, 3vw, 1.5rem);
  right: clamp(0.75rem, 3vw, 1.5rem);
}
.lb-nav { flex: none; }
.lb-counter {
  position: absolute;
  bottom: clamp(0.75rem, 3vw, 1.5rem);
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  color: rgba(244, 241, 234, 0.8);
}
.lightbox :focus-visible { outline: 2px solid var(--bg); outline-offset: 2px; }
@media (max-width: 640px) {
  /* keep the image wide on phones: arrows overlay the backdrop instead of flanking */
  .lightbox { padding: 3.5rem 0.75rem; }
  .lb-nav { position: absolute; top: 50%; transform: translateY(-50%); }
  .lb-prev { left: 0.5rem; }
  .lb-next { right: 0.5rem; }
}
@media (prefers-reduced-motion: reduce) {
  .lightbox, .lb-figure { transition: none; }
  .lb-figure { transform: none; }
}

/* ---- text card + shelf (the `shelf-feature` page block, core/site/blocks.js) ----
   A full-bleed colour band (--sf-bg, set inline from the block's colour) holding a cream
   text card beside a shelf. The card takes an editorial book voice, all Fraunces: display
   optical size (light, tight) for the heading, the site's italic voice for the subline
   (synthesized — no Fraunces italic file ships; see the curved loop), text size for the
   flow text. The shelf column shows an image (picked, else assets/shelf-placeholder.svg);
   with view '3d', assets/shelf-block.js fades a transparent canvas in over it once the
   first frame lands, and the image stays as the loading/no-WebGL state. */
.shelf-feature {
  --sf-bg: #0f8b8d;
  --sf-card: #f3eee3;
  --sf-ink: #1b1a17;
  width: 100vw;
  margin: 6rem 0 6rem calc(50% - 50vw);
  padding: clamp(2.5rem, 6vw, 5rem) clamp(1rem, 4vw, 3rem);
  background: var(--sf-bg);
}
.shelf-feature-inner {
  max-width: 78rem;
  margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: clamp(1.5rem, 4vw, 4rem);
  align-items: stretch;
}
.shelf-feature-card {
  background: var(--sf-card);
  color: var(--sf-ink);
  border-radius: 1.75rem;
  padding: clamp(2rem, 4.5vw, 4.25rem) clamp(1.5rem, 4vw, 4rem);
  box-shadow: 0 28px 60px -34px rgba(0, 0, 0, 0.5);
}
.shelf-feature-h {
  font-family: "Fraunces", Georgia, serif;
  font-weight: 400;
  font-variation-settings: "opsz" 144;
  font-size: var(--section-h);
  line-height: 0.98;
  letter-spacing: -0.025em;
  margin: 0 0 1.1rem;
}
.shelf-feature-sub {
  font-family: "Fraunces", Georgia, serif;
  font-style: italic;
  font-weight: 600;
  font-size: clamp(1.2rem, 2.1vw, 1.6rem);
  line-height: 1.25;
  margin: 0 0 2rem;
  max-width: 26em;
}
.shelf-feature-body {
  font-family: "Fraunces", Georgia, serif;
  font-weight: 450;
  font-size: 1.02rem;
  line-height: 1.5;
  max-width: 34em;
}
.shelf-feature-body p { margin: 0 0 1em; }
.shelf-feature-body > :last-child { margin-bottom: 0; }
.shelf-feature-body a {
  text-decoration-color: var(--sf-bg);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.15em;
}
.shelf-feature--text-right .shelf-feature-card { order: 2; }
.shelf-feature-shelf { position: relative; min-height: 26rem; }
.shelf-feature-img,
.shelf-feature-canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  transition: opacity 0.6s ease;
}
.shelf-feature-img { object-fit: contain; object-position: center bottom; }
.shelf-feature-canvas { display: block; opacity: 0; }
.shelf-feature-shelf[data-shelf-state="live"] .shelf-feature-canvas { opacity: 1; }
.shelf-feature-shelf[data-shelf-state="live"] .shelf-feature-img { opacity: 0; }
@media (max-width: 52rem) {
  .shelf-feature { margin-top: 4rem; margin-bottom: 4rem; }
  .shelf-feature-inner { grid-template-columns: 1fr; }
  .shelf-feature--text-right .shelf-feature-card { order: 0; }
  .shelf-feature-shelf { min-height: 0; height: clamp(20rem, 95vw, 30rem); }
}
@media (prefers-reduced-motion: reduce) {
  .shelf-feature-img, .shelf-feature-canvas { transition: none; }
}

/* ---- the station bands (station layouts 'table' + 'shelf' — hs.featureBand) ----
   The shelf-feature band with a live card: the intro and one panel per project, stacked when
   there's no JS; once assets/feature-card.js runs ([data-gs-live]) only the active view shows.
   The stage beside it is the game shelf (.game-shelf) or the station's 3D table
   (.table-feature). Hovering a thing on it shows the .gs-tip name tag over the canvas; the
   pick buttons are the keyboard route — visually hidden until focused, shown outright when
   WebGL failed. The gs-* classes are the card vocabulary both layouts share. */
.feature-band { scroll-margin-top: 1rem; }
/* The tables stack as one run of colour: no page gap between two bands (Georg 2026-09-28). */
.feature-band:has(+ .feature-band) { margin-bottom: 0; }
.feature-band + .feature-band { margin-top: 0; }
/* A panels text block right after a full-bleed band (the bench, the tables) butts onto it —
   no page gap between the two colour fields (Georg 2026-09-29). */
.bench-show + .text-section--panels,
#stations + .text-section--panels { margin-top: 0; }
#stations:has(+ .text-section--panels) > .feature-band:last-child { margin-bottom: 0; }
.bench-show:has(+ .text-section--panels) { border-bottom: 0; }
.feature-band .shelf-feature-card { display: flex; flex-direction: column; min-height: 36rem; }
/* The stage keeps one size whatever the card holds (a stretching column made it jump and
   re-fit on every pick) and rides along beside a long card. */
.feature-band .shelf-feature-shelf {
  align-self: start;
  position: sticky;
  top: max(1rem, calc(50vh - 20rem));
  height: min(40rem, calc(100vh - 2rem));
  min-height: 26rem;
}
.feature-band[data-gs-live] .gs-view:not(.is-active) { display: none; }
.feature-band[data-gs-live] .gs-view.is-active { animation: gs-in 0.35s ease both; }
.feature-band:not([data-gs-live]) .gs-game { margin-top: 2.5rem; padding-top: 2rem; border-top: 1px solid rgba(27, 26, 23, 0.15); }
.feature-band:not([data-gs-live]) .gs-back { display: none; }
@keyframes gs-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.gs-hint {
  margin: 1.6rem 0 0;
  font-family: "Fraunces", Georgia, serif;
  font-style: italic;
  font-size: 0.95rem;
  opacity: 0.7;
}
.gs-hint::before { content: "← "; }
.shelf-feature--text-left .gs-hint::before { content: ""; }
.shelf-feature--text-left .gs-hint::after { content: " →"; }
.gs-hint.gs-hint--empty::before, .gs-hint.gs-hint--empty::after { content: none; }
.gs-back {
  align-self: flex-start;
  margin: 0 0 1.4rem;
  padding: 0.35rem 0.8rem;
  border: 1px solid rgba(27, 26, 23, 0.25);
  border-radius: 999px;
  background: transparent;
  color: inherit;
  font: 500 0.85rem/1.2 var(--btn-font);
  cursor: pointer;
  transition: background 0.2s, border-color 0.2s;
}
.gs-back:hover, .gs-back:focus-visible { background: rgba(27, 26, 23, 0.06); border-color: rgba(27, 26, 23, 0.5); }
.gs-game { display: flex; flex-direction: column; min-width: 0; }
.gs-title {
  font-family: "Fraunces", Georgia, serif;
  font-weight: 400;
  font-variation-settings: "opsz" 144;
  font-size: clamp(2.2rem, 4.4vw, 3.6rem);
  line-height: 1;
  letter-spacing: -0.02em;
  margin: 0 0 0.6rem;
}
.gs-title:focus { outline: none; }
.gs-meta {
  font-family: "Fraunces", Georgia, serif;
  font-style: italic;
  font-weight: 600;
  font-size: 1.1rem;
  margin: 0 0 1.1rem;
  text-transform: lowercase;
}
.gs-text {
  font-family: "Fraunces", Georgia, serif;
  font-weight: 450;
  font-size: 1.02rem;
  line-height: 1.5;
  margin: 0 0 1.5rem;
  max-width: 34em;
}
.gs-gallery { position: relative; margin: 0 0 1.6rem; min-width: 0; }
.gs-track {
  display: flex;
  gap: 0.6rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
  border-radius: 0.9rem;
}
.gs-track::-webkit-scrollbar { display: none; }
.gs-thumb {
  flex: 0 0 calc(50% - 0.3rem);
  aspect-ratio: 4 / 3;
  scroll-snap-align: start;
  border-radius: 0.9rem;
  overflow: hidden;
  background: rgba(27, 26, 23, 0.08);
  cursor: zoom-in;
}
.gs-gallery:not(.gs-gallery--slides) .gs-thumb:only-child { flex-basis: 100%; aspect-ratio: 16 / 9; }
.gs-thumb img {
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform 0.4s ease;
}
.gs-thumb:hover img, .gs-thumb:focus-visible img { transform: scale(1.04); }
.gs-thumb:focus-visible { outline: 3px solid var(--sf-bg); outline-offset: 2px; }
.gs-gallery-nav { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 0.6rem; }
.gs-arrow {
  width: 2.4rem; height: 2.4rem;
  display: grid; place-items: center;
  border: 1px solid rgba(27, 26, 23, 0.25);
  border-radius: 50%;
  background: transparent;
  color: inherit;
  cursor: pointer;
  transition: background 0.2s, opacity 0.2s;
}
.gs-arrow svg { width: 1.1rem; height: 1.1rem; }
.gs-arrow:hover:not(:disabled), .gs-arrow:focus-visible { background: rgba(27, 26, 23, 0.08); }
.gs-arrow:disabled { opacity: 0.3; cursor: default; }
.gs-more {
  align-self: flex-start;
  margin-top: auto;
  padding: 0.7rem 1.2rem;
  border-radius: 999px;
  background: var(--sf-ink);
  color: var(--sf-card);
  font: 600 0.95rem/1 var(--btn-font);
  text-decoration: none;
  transition: background 0.2s;
}
.gs-more:hover, .gs-more:focus-visible { background: var(--sf-bg); }
.gs-slot .shelf-feature-canvas { touch-action: pan-y; }
.gs-tip {
  position: absolute; left: 0; top: 0;
  z-index: 2;
  padding: 0.35rem 0.75rem;
  border-radius: 0.6rem;
  background: var(--sf-card, #f3eee3);
  color: var(--sf-ink, #1b1a17);
  font: 600 0.9rem/1.2 "Fraunces", Georgia, serif;
  white-space: nowrap;
  pointer-events: none;
  box-shadow: 0 10px 24px -12px rgba(0, 0, 0, 0.55);
}
.gs-tip[hidden] { display: none; }
.gs-picks {
  grid-column: 1 / -1;
  list-style: none;
  margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 0.4rem;
}
.gs-pick {
  padding: 0.35rem 0.8rem;
  border: 0;
  border-radius: 999px;
  background: var(--sf-card);
  color: var(--sf-ink);
  font: 500 0.85rem/1.2 var(--btn-font);
  cursor: pointer;
}
.gs-pick[aria-pressed="true"] { background: var(--sf-ink); color: var(--sf-card); }
/* Live stage: the picks stay for keyboard/screen readers but out of sight until focused. */
.feature-band:has(.gs-slot[data-shelf-state="live"]) .gs-picks:not(:focus-within),
.feature-band:has(.gs-slot[data-shelf-state="idle"]) .gs-picks:not(:focus-within) {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The table stage (.table-feature): the bench canvas fills the column, transparent on the
   band; the station's drawn table (the studio map's watercolor) stands in until the first
   frame and for good without WebGL. The soft ground shadow is .workbench-3d::before. */
/* A table is wide, not tall: the stage takes more of the row than the shelf does, and less
   height. The games table (the game shelf's 'shelf-table' layout) sizes the same. */
.table-feature .shelf-feature-inner,
.game-shelf--table .shelf-feature-inner { grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.25fr); }
.table-feature.shelf-feature--text-right .shelf-feature-inner,
.game-shelf--table.shelf-feature--text-right .shelf-feature-inner { grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.85fr); }
/* Pinned to the top of the row, never sticky: the card is at least as tall as the stage, so a
   longer project panel only grows the band downward — a sticky stage slid down (the whole
   table jumped) every time a pick made the card taller. */
.table-feature .shelf-feature-shelf,
.game-shelf--table .shelf-feature-shelf { position: relative; top: auto; height: min(36rem, calc(100vh - 2rem)); }
.tf-stage .workbench-3d { position: absolute; inset: 0; height: auto; }
/* The ground shadow is drawn in the scene here (it follows the fitted framing). */
.tf-stage .workbench-3d::before { content: none; }
.tf-stage .workbench-ph { display: none; }
.tf-stage .workbench-3d canvas { touch-action: pan-y; }
.tf-ph { padding: 12% 6% 8%; box-sizing: border-box; }
@media (max-width: 52rem) {
  .feature-band .shelf-feature-card { min-height: 0; }
  .shelf-feature--text-left .gs-hint::after { content: " ↓"; }
  .shelf-feature--text-right .gs-hint::before { content: ""; }
  .shelf-feature--text-right .gs-hint::after { content: " ↓"; }
  .feature-band .shelf-feature-shelf { position: relative; top: auto; min-height: 0; height: clamp(22rem, 110vw, 32rem); }
  .table-feature .shelf-feature-inner,
  .table-feature.shelf-feature--text-right .shelf-feature-inner,
  .game-shelf--table .shelf-feature-inner,
  .game-shelf--table.shelf-feature--text-right .shelf-feature-inner { grid-template-columns: 1fr; }
  .table-feature .shelf-feature-shelf,
  .game-shelf--table .shelf-feature-shelf { height: clamp(18rem, 80vw, 28rem); }
  .gs-thumb { flex-basis: calc(70% - 0.3rem); }
}
@media (prefers-reduced-motion: reduce) {
  .feature-band[data-gs-live] .gs-view.is-active { animation: none; }
  .gs-thumb img { transition: none; }
}

/* ---- image + big icons (the `icon-feature` page block, core/site/blocks.js) ----
   A wide rounded panel that breaks out of the 60rem column: the image flush on one side,
   on the other a row of huge marker pictograms (core/site/icon-glyphs.js — strokes in
   currentColor, weight from --if-stroke) over fat Grotesk and a lighter Fraunces italic;
   a soft full-width band of light text closes it. Colours arrive inline (--if-bg,
   --if-band-bg, --if-icon); the --dark modifiers flip the ink on dark picks. */
.icon-feature {
  --if-bg: #fffdf8;
  --if-ink: var(--ink);
  --if-icon: var(--if-ink);
  --if-band-bg: #e7e2d7;
  --if-band-ink: rgba(26, 26, 26, 0.66);
  --if-stroke: 5.5;
  --if-icon-size: clamp(5rem, 11vw, 9rem);
  --if-w: min(76rem, calc(100vw - 2rem));
  width: var(--if-w);
  margin: 6rem 0 6rem calc(50% - var(--if-w) / 2);
  background: var(--if-bg);
  color: var(--if-ink);
  border-radius: 1.5rem;
  overflow: hidden;
  box-shadow: 0 30px 60px -42px rgba(0, 0, 0, 0.4);
}
.icon-feature--bleed { --if-w: 100vw; border-radius: 0; box-shadow: none; }
.icon-feature--dark { --if-ink: #f4f1ea; }
.icon-feature--band-dark { --if-band-ink: rgba(244, 241, 234, 0.8); }
.icon-feature--icons-l { --if-icon-size: clamp(3.75rem, 7vw, 5.5rem); }
.icon-feature--icons-xxl { --if-icon-size: clamp(6rem, 15vw, 12rem); --if-stroke: 5; }
.icon-feature-panel {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
.icon-feature-media { position: relative; min-height: clamp(20rem, 36vw, 32rem); }
.icon-feature-img {
  position: absolute; inset: 0;
  display: block; width: 100%; height: 100%;
  object-fit: cover;
}
.icon-feature--media-right .icon-feature-media { order: 2; }
.icon-feature-copy {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center;
  padding: clamp(2.5rem, 5vw, 4.5rem) clamp(1.5rem, 4vw, 3.5rem);
}
/* --if-n (icons per row, max 4 — set inline by the renderer) lets a full row shrink to fit
   the column instead of wrapping early; the size preset is the ceiling. */
.icon-feature-icons {
  --if-gap: clamp(0.75rem, 3vw, 2.5rem);
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: var(--if-gap);
  width: 100%;
  margin-bottom: clamp(1.5rem, 3.5vw, 2.5rem);
  color: var(--if-icon);
}
.icon-feature-icon {
  --if-fit: var(--if-icon-size);
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--if-fit); height: var(--if-fit);
  transition: transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@supports (width: 1cqw) {
  .icon-feature-icons { container-type: inline-size; }
  .icon-feature-icon {
    --if-fit: min(var(--if-icon-size), calc((100cqw - (var(--if-n, 1) - 1) * var(--if-gap)) / var(--if-n, 1)));
  }
}
.icon-feature-icon svg { width: 100%; height: 100%; stroke-width: var(--if-stroke); overflow: visible; }
.icon-feature-icon:hover { transform: rotate(-8deg) scale(1.08); }
.icon-feature-icon:nth-child(even):hover { transform: rotate(8deg) scale(1.08); }
/* A typed symbol (✌ ☺ ✈ …) — the Unicode heirs of Wingdings, from whatever symbol face
   the reader's system has. */
.icon-feature-char {
  font-family: "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2", "Segoe UI Emoji", "Apple Color Emoji", sans-serif;
  font-size: calc(var(--if-fit) * 0.86);
  line-height: 1;
}
.icon-feature-h {
  font-family: var(--mono);
  font-weight: 850;
  font-size: clamp(2.3rem, 4.8vw, 4.2rem);
  line-height: 0.95;
  letter-spacing: -0.03em;
  margin: 0;
  max-width: 12em;
}
.icon-feature-sub {
  font-family: "Fraunces", Georgia, serif;
  font-style: italic;
  font-weight: 350;
  font-size: clamp(1.15rem, 2vw, 1.55rem);
  line-height: 1.3;
  margin: 0.9rem 0 0;
  max-width: 24em;
  opacity: 0.72;
}
.icon-feature-band {
  background: var(--if-band-bg);
  color: var(--if-band-ink);
  font-family: var(--mono);
  font-weight: 300;
  font-size: clamp(0.98rem, 1.4vw, 1.12rem);
  line-height: 1.5;
  letter-spacing: 0.01em;
  text-align: center;
  padding: clamp(1.1rem, 2.4vw, 1.6rem) clamp(1.25rem, 4vw, 3rem);
}
.icon-feature-band p { margin: 0 auto; max-width: 52em; }
.icon-feature-band p + p { margin-top: 0.6em; }
.icon-feature-band a { color: inherit; text-underline-offset: 0.18em; }
@media (max-width: 52rem) {
  .icon-feature { margin-top: 4rem; margin-bottom: 4rem; }
  .icon-feature-panel { grid-template-columns: 1fr; }
  .icon-feature--media-right .icon-feature-media { order: 0; }
  .icon-feature-media { min-height: 0; aspect-ratio: 4 / 3; }
}
@media (prefers-reduced-motion: reduce) {
  .icon-feature-icon { transition: none; }
  .icon-feature-icon:hover, .icon-feature-icon:nth-child(even):hover { transform: none; }
}

/* ---- project pages (core/site/project-blocks.js; Georg's 2026-09-29 mock) ----------------
   Every project page renders the Pages tab's "Project pages" template: a hero (breadcrumb,
   big grotesk title, the opening paragraph, year + table pills, the photo on an angled panel
   with a hand-written note), the story beside side cards, a gallery carousel. The page runs
   wider than the 60rem column (72rem) and its headings are the heavy grotesk of the mock,
   each with a short orange bar. */
@font-face {
  font-family: "Caveat";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("assets/fonts/caveat-latin-var.woff2") format("woff2");
}
.site-main:has(> .pj-page) { max-width: 72rem; padding-top: 0; }
body:has(.pj-page) .site-footer { margin-top: 2.5rem; }
.pj-page {
  --pj-accent: #ef5a1a;
  --pj-panel: #ede9e1;
  --pj-muted: #4a4640;
}

/* hero: text on the left, the photo bleeding off the right edge and up under the header */
.pj-hero {
  position: relative;
  width: 100vw;
  margin: 0 0 clamp(2.25rem, 4vw, 3.25rem) calc(50% - 50vw);
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  min-height: clamp(20rem, 22vw, 26rem);
  overflow-x: clip; /* the skewed panel's top corner pokes past the right edge */
}
.pj-hero--bare { grid-template-columns: minmax(0, 1fr); min-height: 0; }
.pj-hero-text {
  align-self: center;
  padding: 6.5rem clamp(1.5rem, 3vw, 3rem) 2.25rem max(2rem, calc(50vw - 34rem));
}
.pj-hero--bare .pj-hero-text { padding-right: max(2rem, calc(50vw - 34rem)); }
.pj-crumbs ol {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.5rem;
  margin: 0 0 1rem; padding: 0; list-style: none;
  font-size: 0.82rem; color: rgba(26, 26, 26, 0.58);
}
.pj-crumbs li { min-width: 0; }
.pj-crumbs li + li::before { content: "/"; margin-right: 0.5rem; color: rgba(26, 26, 26, 0.28); }
.pj-crumbs li[hidden] { display: none; }
.pj-crumbs a { color: inherit; text-decoration: none; }
.pj-crumbs a:hover, .pj-crumbs a:focus-visible { color: var(--pj-accent); text-decoration: underline; text-underline-offset: 0.2em; }
.pj-crumbs [aria-current] {
  display: inline-block; max-width: 16rem; vertical-align: bottom;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--ink); font-weight: 600;
}
.pj-page .pj-title {
  margin: 0 0 1.1rem;
  font-family: "Overused Grotesk", system-ui, sans-serif;
  font-weight: 800;
  font-size: clamp(2.2rem, 3.8vw, 3.6rem);
  line-height: 1;
  letter-spacing: -0.03em;
}
.pj-page .pj-title::after { display: none; }
.pj-lede { max-width: 34rem; margin: 0 0 1.35rem; font-size: 1.05rem; line-height: 1.55; color: var(--pj-muted); }
.pj-lede a { color: inherit; text-underline-offset: 0.18em; }
.pj-pills { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0; }
.pj-pill {
  display: inline-flex; align-items: center;
  padding: 0.38rem 0.9rem; border-radius: 999px;
  background: rgba(26, 26, 26, 0.08); color: var(--pj-muted);
  font-size: 0.85rem; font-weight: 500; line-height: 1.2; text-decoration: none;
}
.pj-pill--station { background: color-mix(in srgb, var(--pj-st, #1f5f9e) 13%, #fff); color: var(--pj-st, #1f5f9e); }
.pj-pill--station:hover { background: color-mix(in srgb, var(--pj-st, #1f5f9e) 22%, #fff); }
.pj-pill--link { background: var(--ink); color: var(--bg); }
.pj-pill--link:hover { background: var(--pj-accent); }

.pj-hero-media { position: relative; min-height: 16rem; }
/* The angled panel: skewed box (slanted left edge, rounded foot), the photo counter-skewed
   inside it so it stands upright; the overflow past the right edge is clipped by html. */
.pj-hero-frame {
  position: absolute; inset: 0;
  overflow: hidden;
  transform: skewX(-8deg); transform-origin: 0 100%;
  border-bottom-left-radius: 1.75rem;
  background: #d8cec2;
}
.pj-hero-fill {
  position: absolute; inset: 0 -5rem 0 -1rem;
  transform: skewX(8deg); transform-origin: 0 100%;
}
.pj-hero-fill img, .pj-hero-fill video { width: 100%; height: 100%; max-width: none; object-fit: cover; }
.pj-note {
  position: absolute; left: 11%; top: 17%;
  max-width: 11rem; margin: 0;
  font-family: "Caveat", "Segoe Print", "Bradley Hand", cursive;
  font-weight: 600; font-size: clamp(1.35rem, 1.9vw, 1.75rem); line-height: 1.02;
  color: #1d1a17;
  text-shadow: 0 0 12px rgba(255, 246, 236, 0.75), 0 0 2px rgba(255, 246, 236, 0.9);
  transform: rotate(-9deg); transform-origin: 0 0;
  pointer-events: none;
}
.pj-note-arrow { display: block; width: 3.3rem; height: auto; margin: 0.35rem 0 0 4.6rem; transform: rotate(10deg); }
.pj-hero-credit {
  position: absolute; right: 1rem; bottom: 0.6rem; margin: 0;
  padding: 0.15rem 0.5rem; border-radius: 999px;
  background: rgba(255, 255, 255, 0.78);
}

/* two columns: the story beside the side cards (any page — blocks.js column grouping) */
.page-cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(16rem, 21.5rem);
  gap: clamp(2rem, 5vw, 4.25rem);
  align-items: start;
  margin: 0 0 clamp(2.5rem, 4vw, 3.5rem);
}
.page-cols--no-side { grid-template-columns: minmax(0, 46rem); }
.page-cols--no-main { display: block; }
.page-cols--no-main .page-col-main { display: none; }
.page-cols--no-main .page-col-side { display: grid; grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr)); }
.page-col-main { min-width: 0; }
.page-col-side { display: flex; flex-direction: column; gap: 1.25rem; min-width: 0; }

/* section headings: heavy grotesk + the short orange bar */
.pj-page .pj-h, .pj-page .study-h {
  margin: 2.4rem 0 0.9rem;
  font-family: "Overused Grotesk", system-ui, sans-serif;
  font-weight: 800; font-size: clamp(1.25rem, 1.8vw, 1.45rem);
  letter-spacing: -0.015em; line-height: 1.15;
}
.pj-page .pj-h::after, .pj-page .study-h::after {
  content: ""; display: block;
  width: 1.7rem; height: 3px; margin-top: 0.45rem;
  border-radius: 2px; background: var(--pj-accent);
}
.pj-page h3.pj-h { font-size: 1.1rem; }
.pj-story { color: var(--pj-muted); }
.pj-story > :first-child { margin-top: 0; }
.pj-story p { margin: 0 0 0.75rem; line-height: 1.6; }
.pj-story img { border-radius: 0.75rem; }
.pj-story a { color: var(--ink); text-underline-offset: 0.18em; }
/* lists as tinted panels; data-cols comes from how much they hold */
.pj-list {
  margin: 1rem 0 1.25rem; padding: 0.95rem 1.3rem;
  list-style: none;
  background: var(--pj-panel); border-radius: 0.8rem;
  column-gap: 2.25rem;
}
.pj-list[data-cols="2"] { columns: 2; }
.pj-list[data-cols="3"] { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: 1.5rem; }
.pj-list li {
  position: relative; margin: 0.3rem 0; padding-left: 1.35rem;
  break-inside: avoid;
  font-size: 0.93rem; line-height: 1.45; color: var(--ink);
}
.pj-list[data-cols="3"] li { margin: 0.15rem 0; }
.pj-list li::before {
  content: ""; position: absolute; left: 0.15rem; top: 0.52em;
  width: 0.45rem; height: 0.45rem; border-radius: 50%;
  background: var(--pj-accent);
}
.pj-list li > p { margin: 0; line-height: inherit; }
.pj-list ul { margin: 0.3rem 0 0; padding-left: 1rem; }

/* side cards */
.pj-card { padding: 1.4rem 1.5rem 1.5rem; border-radius: 1rem; background: var(--pj-panel); }
.pj-page .pj-card-h { margin-top: 0; font-size: 1.3rem; }
.pj-card-body { font-size: 0.92rem; color: var(--pj-muted); }
.pj-card-body > :last-child { margin-bottom: 0; }
.pj-card-body p { margin: 0 0 0.9rem; }
.pj-card-body img { width: 100%; border-radius: 0.65rem; }
.pj-zoom { display: block; cursor: zoom-in; border-radius: 0.65rem; }
.pj-zoom:focus-visible { outline: 2px solid var(--pj-accent); outline-offset: 3px; }
.pj-page .pj-tools .pj-card-h { display: flex; align-items: center; gap: 0.6rem; font-size: 1.1rem; }
.pj-page .pj-tools .pj-card-h::after { display: none; }
.pj-card-icon { flex: none; width: 1.25rem; height: 1.25rem; color: #1f4e8c; }
.pj-tags { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 1.05rem 0 0; padding: 0; list-style: none; }
.pj-tags li {
  padding: 0.38rem 0.8rem; border-radius: 0.55rem;
  background: rgba(26, 26, 26, 0.07);
  font-size: 0.85rem; line-height: 1.2;
}
/* the 3D model card: the study viewer on the card's own ground (transparent canvas) */
.pj-model-caption { margin: 0.6rem 0 0; font-size: 0.85rem; color: var(--pj-muted); }
.pj-model-viewer .study-cross { top: 0.5rem; right: 0.5rem; }
.pj-model .study-viewer.pj-model-viewer {
  height: auto; aspect-ratio: 5 / 4;
  border: 0; border-radius: 0.75rem;
  background: radial-gradient(ellipse 70% 60% at 50% 58%, #fbf9f5 0%, rgba(251, 249, 245, 0) 72%);
}
.pj-model-viewer .study-canvas { touch-action: pan-y; }
.pj-model-viewer .study-tip {
  left: 50%; right: auto; bottom: 0.5rem; transform: translateX(-50%);
  flex-direction: row; white-space: nowrap;
  border: 0; background: rgba(255, 255, 255, 0.7);
}
.pj-model-viewer[data-study-state="live"] .study-tip { animation: pj-tip-fade 0.6s ease 4s forwards; }
@keyframes pj-tip-fade { to { opacity: 0; } }

/* the gallery: one sideways-scrolling row, round arrows outside its ends (assets/site.js) */
.pj-gallery { margin: 0 0 clamp(1.5rem, 3vw, 2.5rem); }
.pj-carousel { position: relative; }
.pj-car-track {
  display: flex; gap: 0.75rem;
  margin: 0; padding: 0; list-style: none;
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory; scroll-behavior: smooth;
  scrollbar-width: none;
}
.pj-car-track::-webkit-scrollbar { display: none; }
.pj-car-track:focus-visible { outline: 2px solid var(--pj-accent); outline-offset: 4px; border-radius: 0.5rem; }
.pj-car-item { flex: 0 0 calc((100% - 2.25rem) / 4); scroll-snap-align: start; }
.pj-car-link {
  display: block; overflow: hidden;
  aspect-ratio: 2 / 1; border-radius: 0.5rem;
  background: var(--pj-panel); cursor: zoom-in;
}
.pj-car-link img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.35s ease; }
.pj-car-link:hover img, .pj-car-link:focus-visible img { transform: scale(1.04); }
.pj-car-link:focus-visible { outline: 2px solid var(--pj-accent); outline-offset: 2px; }
.pj-car-btn {
  position: absolute; top: 50%; z-index: 2;
  display: grid; place-items: center;
  width: 2.1rem; height: 2.1rem; padding: 0;
  border: 1px solid rgba(26, 26, 26, 0.16); border-radius: 50%;
  background: #fff; color: var(--ink);
  box-shadow: 0 2px 8px rgba(26, 26, 26, 0.08);
  transform: translateY(-50%);
  cursor: pointer;
  transition: opacity 0.2s ease, border-color 0.15s ease;
}
.pj-car-btn[hidden] { display: none; }
.pj-car-btn svg { width: 1rem; height: 1rem; }
.pj-car-btn:hover { border-color: var(--ink); }
.pj-car-btn:disabled { opacity: 0.3; cursor: default; }
.pj-car-prev { left: -2.6rem; }
.pj-car-next { right: -2.6rem; }

/* the log, study and social proof inside the main column */
.pj-page .pj-log { margin-top: 0; }
.pj-page .post-block .post-title { font-family: "Overused Grotesk", system-ui, sans-serif; font-weight: 800; letter-spacing: -0.015em; }
.pj-page .study { margin-top: 0; }
.pj-page .page-col-main > * + .pj-log,
.pj-page .page-col-main > * + .study { margin-top: 1rem; }

@media (max-width: 76rem) {
  .pj-car-prev { left: 0.5rem; }
  .pj-car-next { right: 0.5rem; }
}
@media (max-width: 60rem) {
  .page-cols { grid-template-columns: minmax(0, 1fr); }
  .page-cols--no-side { grid-template-columns: minmax(0, 1fr); }
  .page-col-side { display: grid; grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr)); }
  .pj-car-item { flex-basis: calc((100% - 1.5rem) / 3); }
}
@media (max-width: 48rem) {
  .pj-hero { grid-template-columns: minmax(0, 1fr); min-height: 0; }
  .pj-hero-text { padding: 5.5rem 2rem 1.75rem; }
  .pj-hero-media { height: clamp(15rem, 62vw, 24rem); margin-left: 2rem; }
  .pj-hero-frame { transform: skewX(-6deg); }
  .pj-hero-fill { transform: skewX(6deg); }
  .pj-note { left: 9%; top: 12%; }
  .pj-list[data-cols="2"] { columns: 1; }
  .pj-list[data-cols="3"] { grid-template-columns: minmax(0, 1fr); }
  .pj-car-item { flex-basis: calc((100% - 0.75rem) / 2); }
  .pj-car-link { aspect-ratio: 4 / 3; }
}
@media (max-width: 40rem) {
  .pj-hero-text { padding-left: 1rem; padding-right: 1rem; }
  .pj-hero-media { margin-left: 1rem; }
  .pj-car-item { flex-basis: 78%; }
}
@media (prefers-reduced-motion: reduce) {
  .pj-car-track { scroll-behavior: auto; }
  .pj-car-link img { transition: none; }
}

/* ---- about page (core/site/about-blocks.js) ----
   Georg's watercolour about sketch (2026-09-29): the parrot hero with the round portrait,
   a band of icon columns, a quote card beside a photo, the CV on a paper band, ribbon
   headings, the favourite projects in a pencil frame, milestones on a brush stroke and a
   tagline between two leaf sprays, over the watercolour footer. The paintings live in
   assets/about/ (keyed to transparency, so they sit on any ground). Bands break out of
   .site-main's column like .hero; their content re-centres to ~74rem.
   Colours follow the front page (Georg 2026-10-01): its warm panel paper for the bands, ink
   pill buttons, and the four tables' colours (core/site/stations.js accents) on the icons,
   the milestones and the favourite cards — each picks one through --ab-c. */
:root {
  --ab-paper: #efe4cf;   /* the paper bands (icon columns, CV, tagline) — the front page's warm panel */
  --ab-cream: #f6ecd8;   /* the hero's paper, where the painting runs out */
  --ab-teal: #0a857e;    /* games */
  --ab-red: #b3261e;     /* soldering */
  --ab-blue: #1f5f9e;    /* professional */
  --ab-orange: #c05a14;  /* fabrication */
  --ab-line: rgba(26, 26, 26, 0.13);
  --ab-wide: min(74rem, 100vw - 2.5rem);
  --ab-mid: min(66rem, 100vw - 2.5rem);
}

/* hero: portrait · headline + intro + buttons · the painted parrot */
.ab-hero {
  position: relative;
  width: 100vw;
  margin: -6.5rem 0 0 calc(50% - 50vw); /* under the floating nav, like the home hero */
  padding: 8rem clamp(1.25rem, 5vw, 6rem) 4.5rem;
  min-height: clamp(32rem, 47vw, 46rem);
  display: flex; align-items: center;
  background: var(--ab-cream) url("assets/about/hero-parrot.webp") 70% 58% / cover no-repeat;
}
.ab-hero--plain { background-image: none; }
.ab-hero-inner {
  display: grid;
  /* narrow enough to stay on the plain paper left of the parrot (it starts ~55% across) */
  grid-template-columns: clamp(9rem, 15vw, 17rem) minmax(0, min(27rem, 30vw));
  gap: clamp(1.5rem, 3vw, 4rem);
  align-items: center;
}
.ab-hero--no-portrait .ab-hero-inner { grid-template-columns: minmax(0, min(34rem, 50vw)); }
.ab-hero-portrait {
  margin: 0;
  aspect-ratio: 1;
  border-radius: 50%;
  overflow: hidden;
  /* a soft, painted-looking edge instead of a hard crop */
  -webkit-mask-image: radial-gradient(closest-side, #000 93%, transparent 100%);
  mask-image: radial-gradient(closest-side, #000 93%, transparent 100%);
}
.ab-hero-portrait img { width: 100%; height: 100%; object-fit: cover; }
.ab-hero-h {
  margin: 0 0 1.1rem;
  font-size: clamp(2.5rem, 5.4vw, 4.6rem);
  font-weight: 500;
  line-height: 0.98;
  letter-spacing: -0.025em;
  text-wrap: balance;
}
.ab-hero-h::after { display: none; }
.ab-hero-h strong { font-weight: 800; }
.ab-dot { color: var(--accent); }
.ab-hero-body { max-width: 30rem; font-size: 1.02rem; line-height: 1.55; color: rgba(26, 26, 26, 0.82); }
.ab-hero-body p { margin: 0 0 0.6rem; }
.ab-hero-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 1.5rem 0 0; }
.ab-btn {
  display: inline-flex; align-items: center;
  padding: 0.72rem 1.45rem;
  border: 1.5px solid var(--ink);
  border-radius: 999px;
  font-family: var(--btn-font); font-size: 1rem; font-weight: 600; line-height: 1.2;
  text-decoration: none;
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.ab-btn--solid { background: var(--ink); color: var(--bg); }
.ab-btn--solid:hover { background: #333; border-color: #333; transform: translateY(-1px); }
.ab-btn--ghost { background: rgba(244, 241, 234, 0.7); color: var(--ink); }
.ab-btn--ghost:hover { background: var(--ink); color: var(--bg); transform: translateY(-1px); }
.ab-btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* icon columns: a paper band, hairlines between the columns */
.ab-pillars {
  width: 100vw;
  margin: 0 0 0 calc(50% - 50vw);
  padding: 2.6rem max(1.25rem, calc(50vw - 37rem));
  background: var(--ab-band, var(--ab-paper));
  border-bottom: 1px solid var(--ab-line);
}
.ab-pillars-list {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(var(--ab-n, 3), minmax(0, 1fr));
}
.ab-pillar {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 1.1rem;
  padding: 0.2rem clamp(1rem, 2.6vw, 2.5rem);
  border-left: 1px solid var(--ab-line);
}
.ab-pillar:first-child { border-left: 0; padding-left: 0; }
.ab-pillar:last-child { padding-right: 0; }
.ab-pillar:nth-child(4n+1) { --ab-c: var(--ab-teal); }
.ab-pillar:nth-child(4n+2) { --ab-c: var(--ab-blue); }
.ab-pillar:nth-child(4n+3) { --ab-c: var(--ab-orange); }
.ab-pillar:nth-child(4n+4) { --ab-c: var(--ab-red); }
.ab-pillar-icon svg { width: 2.6rem; height: 2.6rem; stroke-width: 3.4; color: var(--ab-c, var(--ink)); }
.ab-pillar-h { margin: 0.35rem 0 0.45rem; font-size: 1.45rem; font-weight: 600; letter-spacing: -0.01em; }
.ab-pillar-text { margin: 0 0 0.75rem; font-size: 0.93rem; line-height: 1.5; color: rgba(26, 26, 26, 0.72); }
.ab-pillar-link {
  font-size: 0.9rem; font-weight: 700;
  text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.3em;
}
.ab-pillar-link:hover { text-decoration-color: var(--ab-c, var(--accent)); }
.ab-arrow { display: inline-block; margin-left: 0.4em; color: var(--ab-c, var(--accent)); text-decoration: none; transition: transform 0.2s ease; }
.ab-pillar-link:hover .ab-arrow { transform: translateX(3px); }

/* quote + photo: the hand-drawn quote frame as a 9-slice border (the quote marks and corners
   stay crisp, only the lines stretch), a leaf spray peeking in on the left */
.ab-quote { position: relative; padding: 5.5rem 0 4.5rem; scroll-margin-top: 2rem; }
.ab-quote--leaves::before {
  content: ""; position: absolute; z-index: -1;
  left: calc(50% - 50vw); top: 1.5rem;
  width: clamp(6rem, 13vw, 11rem); aspect-ratio: 378 / 560;
  background: url("assets/about/leaves-left.webp") left top / contain no-repeat;
  opacity: 0.38;
  pointer-events: none;
}
.ab-quote-inner {
  width: var(--ab-mid);
  margin-left: calc(50% - min(33rem, 50vw - 1.25rem));
  display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: clamp(2rem, 4.5vw, 4rem); align-items: center;
}
.ab-quote--no-media .ab-quote-inner { grid-template-columns: minmax(0, 40rem); justify-content: center; }
.ab-quote--media-left .ab-quote-media { order: -1; }
.ab-quote-card {
  border-style: solid;
  border-width: 60px 22px 25px 50px;
  border-image: url("assets/about/quote-frame.webp") 110 40 45 90 / 60px 22px 25px 50px stretch;
  padding: 0.2rem 1.4rem 1.1rem 0.9rem;
}
.ab-quote-h {
  margin: -0.5rem 0 0.85rem;
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(1.75rem, 3vw, 2.4rem); font-weight: 800;
  line-height: 1.05; letter-spacing: -0.015em;
}
.ab-quote-body { font-size: 0.98rem; line-height: 1.6; color: rgba(26, 26, 26, 0.8); }
.ab-quote-body p { margin: 0 0 0.75rem; }
.ab-quote-body p:last-child { margin-bottom: 0; }
.ab-quote-media { margin: 0; }
.ab-quote-media img {
  width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
  border-radius: 6px;
  box-shadow: 0 18px 36px -22px rgba(60, 40, 20, 0.55);
}
.ab-quote-media figcaption {
  margin-top: 0.7rem;
  font-family: "Fraunces", Georgia, serif; font-style: italic; font-size: 0.85rem;
  color: rgba(26, 26, 26, 0.6);
  text-wrap: pretty;
}

/* CV: two columns on a paper band; uppercase serif headings with their pictogram */
.ab-cv {
  width: 100vw;
  margin: 1.5rem 0 0 calc(50% - 50vw);
  padding: 4.25rem max(1.25rem, calc(50vw - 34rem)) 4.5rem;
  background: var(--ab-band, var(--ab-paper));
  border-top: 1px solid var(--ab-line);
  border-bottom: 1px solid var(--ab-line);
}
.ab-cv-inner { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 3rem clamp(2rem, 5vw, 4.5rem); }
.ab-cv--single .ab-cv-inner { grid-template-columns: minmax(0, 42rem); }
.ab-cv-sec { scroll-margin-top: 2rem; }
.ab-cv-sec + .ab-cv-sec { margin-top: 2.6rem; }
.ab-cv-col--right .ab-cv-sec + .ab-cv-sec { padding-top: 2.4rem; border-top: 1px solid var(--ab-line); }
.ab-cv-col .ab-cv-sec:nth-child(4n+1) { --ab-c: var(--ab-teal); }
.ab-cv-col .ab-cv-sec:nth-child(4n+2) { --ab-c: var(--ab-red); }
.ab-cv-col .ab-cv-sec:nth-child(4n+3) { --ab-c: var(--ab-orange); }
.ab-cv-col .ab-cv-sec:nth-child(4n+4) { --ab-c: var(--ab-blue); }
.ab-cv-col--right .ab-cv-sec:nth-child(4n+1) { --ab-c: var(--ab-blue); }
.ab-cv-col--right .ab-cv-sec:nth-child(4n+2) { --ab-c: var(--ab-orange); }
.ab-cv-col--right .ab-cv-sec:nth-child(4n+3) { --ab-c: var(--ab-teal); }
.ab-cv-col--right .ab-cv-sec:nth-child(4n+4) { --ab-c: var(--ab-red); }
.ab-cv-h {
  display: flex; align-items: center; gap: 0.6rem;
  margin: 0 0 1rem;
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(1.55rem, 2.6vw, 2.1rem); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.01em; line-height: 1;
}
.ab-cv-icon svg { width: 1em; height: 1em; stroke-width: 3.6; color: var(--ab-c, var(--ink)); }
.ab-cv-body { font-size: 0.94rem; line-height: 1.5; }
.ab-cv-body p { margin: 0 0 0.7rem; color: rgba(26, 26, 26, 0.78); }
.ab-cv-body ul { margin: 0 0 0.7rem; padding-left: 1.1rem; }
.ab-cv-body li { margin: 0.32rem 0; }
.ab-cv-body li::marker { color: var(--ab-c, var(--accent)); }
.ab-cv-body a { text-decoration-color: rgba(26, 26, 26, 0.35); text-underline-offset: 0.2em; }
.ab-cv-body a:hover { text-decoration-color: var(--ab-c, var(--accent)); }

/* ribbon headings: the painted banner stretches to its text; the paddings keep the words on
   the band (leaf: the spray on the right; leaves: one on each side) */
.ab-ribbon-wrap { margin: 4.5rem 0 2.25rem; text-align: center; scroll-margin-top: 2rem; }
.ab-ribbon {
  display: inline-block;
  margin: 0;
  font-size: clamp(1.55rem, 3.3vw, 2.4rem);
  line-height: 1.1;
  background: url("assets/about/ribbon-leaf.webp") center / 100% 100% no-repeat;
  padding: 1.2em 4.1em 1.05em 1.75em;
}
.ab-ribbon--leaves {
  background-image: url("assets/about/ribbon-leaves.webp");
  padding: 1.25em 2.5em 1.1em;
}
.ab-ribbon--serif { font-weight: 500; letter-spacing: -0.01em; }
.ab-ribbon--bold { font-weight: 800; letter-spacing: -0.01em; }
.ab-ribbon-text { display: inline-block; transform: translateY(0.08em); }

/* milestones: dots riding the brush stroke (placed from the stroke's measured centre line),
   each entry centred under its dot; dot and year share a table colour (an eggshell ring keeps
   the orange one off the orange paint) */
.ab-ms-dot:nth-child(4n+1), .ab-ms:nth-child(4n+1) { --ab-c: var(--ab-teal); }
.ab-ms-dot:nth-child(4n+2), .ab-ms:nth-child(4n+2) { --ab-c: var(--ab-red); }
.ab-ms-dot:nth-child(4n+3), .ab-ms:nth-child(4n+3) { --ab-c: var(--ab-blue); }
.ab-ms-dot:nth-child(4n+4), .ab-ms:nth-child(4n+4) { --ab-c: var(--ab-orange); }
.ab-milestones { width: var(--ab-wide); margin: 0.5rem 0 4.5rem calc(50% - min(37rem, 50vw - 1.25rem)); }
.ab-ms-stroke {
  position: relative;
  aspect-ratio: 1800 / 168;
  background: url("assets/about/timeline-stroke.webp") center / 100% 100% no-repeat;
}
.ab-ms-dot {
  position: absolute;
  width: clamp(0.95rem, 1.7vw, 1.45rem); aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border-radius: 47% 53% 45% 55% / 55% 45% 53% 47%;
  background: radial-gradient(circle at 36% 34%, color-mix(in srgb, var(--ab-c, var(--ink)) 62%, #fff), var(--ab-c, var(--ink)) 62%);
  box-shadow: 0 0 0 3px var(--bg);
}
.ab-ms-list {
  list-style: none; margin: 1.1rem 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(var(--ab-n, 4), minmax(0, 1fr)); gap: 1.25rem;
  text-align: center;
}
.ab-ms-when {
  margin: 0 0 0.3rem;
  font-family: "Fraunces", Georgia, serif; font-size: 1.15rem; font-weight: 800; letter-spacing: -0.01em;
  color: var(--ab-c, var(--ink));
}
.ab-ms-title { margin: 0 0 0.25rem; font-size: 1.05rem; font-weight: 700; }
.ab-ms-text { margin: 0 auto; max-width: 17rem; font-size: 0.87rem; line-height: 1.45; color: rgba(26, 26, 26, 0.66); }
.ab-ms-text a { color: var(--ink); text-decoration-color: var(--ab-c, var(--accent)); text-underline-offset: 0.2em; }
.ab-ms-text a:hover { color: var(--ab-c, var(--accent)); }

/* tagline band: italic serif over a short brush-stroke underline, leaf sprays in the corners
   reaching down over the footer's wash */
.ab-tagline {
  position: relative;
  width: 100vw;
  margin: 2.5rem 0 0 calc(50% - 50vw);
  padding: 4.5rem 1.25rem 6rem;
  background: var(--ab-band, var(--ab-paper));
  text-align: center;
}
.ab-tagline-text {
  margin: 0;
  font-family: "Fraunces", Georgia, serif; font-style: italic; font-weight: 500;
  font-size: clamp(1.7rem, 4.2vw, 3.1rem); line-height: 1.15; letter-spacing: -0.015em;
}
.ab-tagline-line {
  display: inline-block;
  padding: 0 0.3em 0.62em;
  /* the timeline's brush stroke, flattened into an underline clear of the descenders */
  background: url("assets/about/timeline-stroke.webp") 62% 100% / 58% 0.42em no-repeat;
}
.ab-tagline-note {
  max-width: 36rem; margin: 1.6rem auto 0;
  font-size: 0.98rem; line-height: 1.55; color: rgba(26, 26, 26, 0.72);
}
.ab-tagline-note p { margin: 0 0 0.4rem; }
.ab-tagline-note a { text-decoration-color: var(--accent); text-underline-offset: 0.2em; }
.ab-tagline--leaves::before,
.ab-tagline--leaves::after {
  content: ""; position: absolute; z-index: 3;
  bottom: -3.5rem;
  width: clamp(6.5rem, 14vw, 12.5rem); aspect-ratio: 378 / 560;
  background: url("assets/about/leaves-left.webp") left bottom / contain no-repeat;
  pointer-events: none;
}
.ab-tagline--leaves::before { left: 0; }
.ab-tagline--leaves::after { right: 0; background-image: url("assets/about/leaves-right.webp"); background-position: right bottom; aspect-ratio: 390 / 560; }

/* featured work as the sketch's favourite projects: four white cards (cover on top) inside a
   pencil double frame; the label is the small kicker over each title */
.fw-card-label {
  display: block;
  margin: 0 0 0.25rem;
  font-family: var(--mono); font-size: 0.7rem; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--fw-st, var(--accent));
}
.featured-work--4 .featured-work-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); column-gap: 1.25rem; }
.featured-work--sketch { padding: 0 0 3rem; }
.fw-framed {
  position: relative;
  width: var(--ab-wide);
  margin-left: calc(50% - min(37rem, 50vw - 1.25rem));
  padding: clamp(1.25rem, 2.6vw, 2.25rem) clamp(1rem, 2.6vw, 2.5rem);
}
.fw-frame {
  position: absolute; inset: 0; width: 100%; height: 100%;
  overflow: visible;
  fill: none; stroke: #2e2419; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
  pointer-events: none;
}
.fw-frame path { vector-effect: non-scaling-stroke; }
.fw-frame path + path { stroke-width: 1.25; opacity: 0.6; }
.featured-work--sketch .fw-card {
  background: #fffdf8;
  border-radius: 6px;
  box-shadow: 0 1px 2px rgba(60, 40, 20, 0.1), 0 12px 26px -16px rgba(60, 40, 20, 0.4);
  overflow: hidden;
}
.featured-work--sketch .fw-card-media { order: -1; background: rgba(60, 40, 20, 0.08); }
.featured-work--sketch .fw-card-media img, .featured-work--sketch .fw-card-media .ph { aspect-ratio: 4 / 3; }
.featured-work--sketch .fw-card-title {
  align-self: start;
  margin: 0;
  padding: 1rem 1.1rem 0.35rem;
  font-weight: 600; font-size: 1.3rem;
}
.featured-work--sketch .fw-card-body { padding: 0 1.1rem 1.2rem; }
.featured-work--sketch .fw-card-text { font-size: 0.88rem; -webkit-line-clamp: 2; }
.featured-work--sketch .fw-card:has(.fw-card-link:focus-visible) { outline-offset: 3px; }

/* A page ending on the tagline band (the about page): the band sits right on the footer and
   the footer's ragged top paints over the band's foot, not the page ground. */
.site-main .ab-tagline:last-of-type { margin-bottom: -2rem; }
body:has(.site-main .ab-tagline:last-of-type) .site-footer { margin-top: -3rem; }
/* Narrow screens make the footer taller than the painting is wide; there the texture is drawn
   taller than the band so its pale top and bottom streaks fall outside it (the edge strips
   still give the ragged outline) and the last links stay on dark paint. */
@media (max-width: 48rem) {
  .site-footer::before { background-size: auto 135%; background-position: 50% 42%; }
}

@media (max-width: 64rem) {
  /* tablets: the parrot moves into the right half, fading in from the left; the portrait
     stacks over the text in the left half */
  .ab-hero { background-image: none; }
  .ab-hero--parrot::before {
    content: ""; position: absolute; top: 0; bottom: 0; right: 0; left: 44%;
    background: url("assets/about/hero-parrot.webp") 88% 62% / cover no-repeat;
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 32%);
    mask-image: linear-gradient(90deg, transparent, #000 32%);
    pointer-events: none;
  }
  .ab-hero-inner { position: relative; width: 56%; grid-template-columns: minmax(0, 1fr); gap: 1.25rem; }
  .ab-hero-portrait { width: clamp(8rem, 18vw, 11rem); }
  .ab-pillar { grid-template-columns: minmax(0, 1fr); gap: 0.4rem; }
  .ab-pillar-icon svg { width: 2.2rem; height: 2.2rem; }
  .featured-work--4 .featured-work-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 1.5rem; }
  .featured-work--4 .fw-card { row-gap: 0; }
}
@media (max-width: 48rem) {
  .ab-hero-inner { width: 58%; }
  .ab-ribbon { font-size: clamp(1.25rem, 5.6vw, 1.55rem); }
  .ab-pillars-list { grid-template-columns: minmax(0, 1fr); gap: 1.75rem; }
  .ab-pillar, .ab-pillar:first-child {
    grid-template-columns: auto minmax(0, 1fr); gap: 1rem;
    padding: 0; border-left: 0;
  }
  .ab-pillar + .ab-pillar { padding-top: 1.75rem; border-top: 1px solid var(--ab-line); }
  .ab-quote { padding: 3.5rem 0 3rem; }
  .ab-quote-inner { grid-template-columns: minmax(0, 1fr); }
  .ab-quote--media-left .ab-quote-media { order: 0; }
  .ab-cv-inner { grid-template-columns: minmax(0, 1fr); }
  .ab-cv-col--right .ab-cv-sec:first-child { padding-top: 2.4rem; border-top: 1px solid var(--ab-line); }
  /* milestones turn into a vertical line of dots */
  .ab-ms-stroke { display: none; }
  .ab-ms-list {
    position: relative;
    grid-template-columns: minmax(0, 1fr); gap: 1.6rem;
    padding-left: 2.25rem;
    text-align: left;
  }
  .ab-ms-list::before {
    content: ""; position: absolute; left: 0.55rem; top: 0.2rem; bottom: 0.2rem;
    width: 7px; border-radius: 4px;
    background: linear-gradient(#c4621b, #efa55a);
    opacity: 0.85;
  }
  .ab-ms { position: relative; }
  .ab-ms::before {
    content: ""; position: absolute; left: -1.95rem; top: 0.2rem;
    width: 1.05rem; aspect-ratio: 1;
    border-radius: 47% 53% 45% 55% / 55% 45% 53% 47%;
    background: radial-gradient(circle at 36% 34%, color-mix(in srgb, var(--ab-c, var(--ink)) 62%, #fff), var(--ab-c, var(--ink)) 62%);
    box-shadow: 0 0 0 3px var(--bg);
  }
  .ab-ms-text { margin: 0; }
  .ab-tagline { padding: 3.5rem 1.25rem 5rem; }
  .featured-work--4 .featured-work-grid { grid-template-columns: minmax(0, 1fr); row-gap: 1.5rem; }
}
@media (max-width: 40rem) {
  /* phones: the painting on top — parrot beside the portrait — fading into paper under
     the text */
  .ab-hero {
    display: block;
    min-height: 0;
    padding: 6.25rem 1.25rem 3rem;
    /* the painting is at most 25rem tall (width follows its 1.75 aspect), so wider phones
       don't get a parrot taller than the portrait and the text over it */
    background:
      linear-gradient(to bottom, rgba(246, 236, 216, 0) 55%, var(--ab-cream) 96%) 0 0 / 100% min(100vw, 25rem) no-repeat,
      var(--ab-cream) url("assets/about/hero-parrot-1200.webp") 74% 0 / auto min(100vw, 25rem) no-repeat;
  }
  .ab-hero--plain { background: var(--ab-cream); }
  .ab-hero-copy { max-width: 24rem; }
  .ab-hero--parrot::before { content: none; }
  .ab-hero-inner { width: auto; grid-template-columns: minmax(0, 1fr); gap: 1.4rem; }
  .ab-hero-portrait { width: clamp(8.5rem, 40vw, 11rem); }
  .ab-hero-h { font-size: clamp(2.4rem, 11vw, 3.2rem); }
}
@media (prefers-reduced-motion: reduce) {
  .ab-btn, .ab-arrow { transition: none; }
}
