/* ============================================================
   AGORA — style.css
   A human-first digital public square.
   Mobile-first, vanilla CSS. No frameworks, no build step.

   Breakpoints (mobile-first, min-width unless noted):
     ≤480px   phone       — tuned in section 22 (max-width overrides)
     ≤768px   tablet      — hamburger nav, single-column layouts
     ≥769px   desktop nav — inline links + CTA
     ≥1024px  wide        — two-column hero, three-column gallery

   Palette (shared with the Agora app + logo):
     deep dark surfaces · translucent glass · vibrant purple actions
   ============================================================ */

/* ---------- 1. Tokens ---------- */
:root {
  /* ---- 1a. Colour channels -------------------------------------------
     Every translucent tint in this file is composed as
     rgba(var(--x-rgb), <alpha>) so a theme only has to redefine the
     channel once and all of its alpha variants follow. This is what makes
     the light theme a variable swap rather than a second stylesheet.    */
  --bg-rgb: 10, 11, 14;
  --bg-soft-rgb: 14, 15, 21;
  --surface-rgb: 19, 20, 29;
  --surface-2-rgb: 25, 27, 39;
  --surface-3-rgb: 36, 38, 51;
  --text-rgb: 243, 244, 246;
  --muted-rgb: 156, 163, 175;
  --accent-rgb: 139, 92, 246;
  --accent-deep-rgb: 124, 58, 237;
  --blue-rgb: 96, 165, 250;
  --lavender-rgb: 196, 181, 253;
  --violet-soft-rgb: 167, 139, 250;
  --refuse-rgb: 244, 63, 94;
  --build-rgb: 16, 185, 129;

  /* Specular rim for edges drawn ON a saturated fill. This one deliberately
     does not invert with --overlay-rgb: the brand-gradient chip stays deep in
     both themes, so its rim has to stay bright in both. Letting it follow
     --lavender-rgb would make the light theme's rim the same violet as the
     gradient stop underneath it, and the edge would simply disappear.   */
  --rim-rgb: 196, 181, 253;

  /* White-based glass in the dark theme. The light theme flips this single
     channel to a dark slate, which inverts every border, sheen, hairline
     and glass panel in one move — the inversion the light theme lives on. */
  --overlay-rgb: 255, 255, 255;

  /* Emissive bloom colour for glows and drop-shadows. Dark theme: violet
     light bleeding outward. Light theme: a deep slate, so the identical
     alpha values read as diffused drop-shadows instead of neon bloom.    */
  --glow-rgb: 139, 92, 246;
  --glow-blue-rgb: 96, 165, 250;
  --shadow-rgb: 5, 3, 9;

  /* ---- 1b. Surfaces & lines ------------------------------------------ */
  --bg: #0A0B0E;
  --bg-soft: #0E0F15;
  --surface: #13141D;
  --surface-2: #191B27;
  --surface-3: #242633;
  /* Form controls sit recessed against the card they live in. */
  --field-bg: #0A0B0E;
  --border: rgba(255, 255, 255, 0.08);
  --border-strong: rgba(255, 255, 255, 0.16);

  /* ---- 1c. Type ------------------------------------------------------- */
  --text: #F3F4F6;
  --text-dim: #9CA3AF;
  --text-faint: #9CA3AF;
  --text-bright: #FAF7FF;
  /* Ink used ON an accent fill. Dark plum on violet here; the light theme
     flips it to white because its accent is deep enough to carry it.     */
  --on-accent: #110819;
  /* Ink used on the saturated brand gradient (rotary role sign). */
  --on-gradient: #FFFFFF;

  /* ---- 1d. Brand & semantics ------------------------------------------ */
  --accent: #8B5CF6;
  --accent-deep: #7C3AED;
  --accent-soft: rgba(var(--accent-rgb), 0.08);
  --blue: #60A5FA;
  --lavender: #C4B5FD;
  --violet-soft: #A78BFA;
  --brand-gradient: linear-gradient(90deg, #7C3AED 0%, #8B5CF6 48%, #60A5FA 100%);
  /* Deeper sibling of the brand gradient for text-bearing chips, so the
     ink on top clears WCAG AA at every stop. */
  --brand-gradient-ink: linear-gradient(105deg, #7C3AED, #8B5CF6 58%, #60A5FA);
  /* One frosted-glass recipe for every translucent surface on the site.
     The fill is deliberately see-through: a panel that is 88% opaque hides
     the blur behind it, so the glass reads as flat paint. At 66% the same
     blur visibly softens whatever passes underneath, matching the liquid
     field around the pointer. */
  --glass: rgba(var(--surface-rgb), 0.66);
  --glass-deep: rgba(28, 25, 45, 0.74);
  --glass-filter: blur(18px) saturate(160%);
  --refuse: #FB7185;
  --refuse-border: rgba(var(--refuse-rgb), 0.25);
  --build: #6EE7B7;
  --build-border: rgba(var(--build-rgb), 0.25);
  color-scheme: dark;

  /* ---- 1e. Device mockup ----------------------------------------------
     The phone frame is hardware around dark app screenshots, so it keeps
     its dark finish in BOTH themes; a pale bezel around a dark screen
     reads as broken rather than designed. Deliberately not overridden.  */
  --device-hi: #242633;
  --device-mid: #13141D;
  --device-lo: #0A0B0E;
  --device-screen: #0A0B0E;
  --device-ring-rgb: 255, 255, 255;
  /* Ink and tint painted INSIDE that screen. These must not follow the
     palette: --text-dim flips to dark slate in the light theme, which on a
     screen that stays near-black would render the pending-state caption
     dark-on-dark. Pinned to the dark theme's own values instead.        */
  --device-ink: #9CA3AF;
  --device-ink-dim: #9CA3AF;
  --device-accent-rgb: 139, 92, 246;
  --device-accent-soft: rgba(var(--device-accent-rgb), 0.08);

  /* ---- 1f. Cursor disturbance -----------------------------------------
     The pointer paints no body of its own any more -- it is a backdrop
     refraction plus one hairline ring -- so the only channel it needs is the
     edge colour of that ring, kept per theme so it never glows white on white. */
  --cursor-lens-edge-rgb: 255, 255, 255;
  /* The solid lavender bead needs a crisp white ring and a black outer border,
     exactly like the reference cursor. Kept per theme so the bead never reads
     as a hole or a second circle. */
  --cursor-lens-ring-rgb: 255, 255, 255;
  --cursor-lens-ring-outer-rgb: 0, 0, 0;

  /* ---- 1g. Masks & embedded assets ------------------------------------
     #000 here is an ALPHA carrier for mask-image, not a theme colour: the
     mask paints where it is opaque. It must stay fully opaque in every
     theme, so it is tokenised but intentionally never overridden.

     The select chevron is an inline SVG. A data URI cannot contain var(),
     so the whole URL becomes the token and each theme supplies its own —
     otherwise a %23-encoded colour slips past any "no hardcoded colours"
     audit while still painting a theme-specific value.                  */
  --mask-solid: #000;
  --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' stroke='%239aa8bf' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");


  /* ---- 1h. Site mesh ----
     The background is a feature, not a texture, and these are authored to be
     conspicuous against the dark canvas: lines at 0.36-0.43 alpha and vertices at
     0.45-0.60 over near-black. Tokenised rather than hardcoded in the canvas for
     two reasons -- the light theme needs deep equivalents instead of dark-theme
     neon on white, and the whole background should be retunable from one place.

     The line channel is the site's own --accent-rgb rather than a bespoke indigo,
     so the lattice is on-brand instead of sitting next to the brand. The glow is a
     step MORE saturated than the core it blooms around, which is what makes a halo
     read as coloured light rather than as a grey smudge. */
  --mesh-node-rgb: 167, 139, 250;      /* violet-400: the vertex core */
  --mesh-node-hi-rgb: 196, 181, 253;   /* lavender: the quarter of vertices that catch light */
  --mesh-glow-rgb: 124, 58, 237;       /* violet-600: the bloom, saturated on purpose */
  --mesh-line-rgb: 139, 92, 246;       /* == --accent-rgb, so the lattice is on-brand */
  /* How hard the whole layer presses on the page -- one dial multiplying every
     line and vertex alpha, so a theme can match PRESENCE rather than numbers.

     Both themes sit at 1 now, and that is a consequence of the calibration rather
     than an oversight. When the alphas were 0.45-0.60 the light theme needed 0.55:
     violet-800 at that strength over off-white measures 3.02:1 and prints a
     lavender graph across body copy. At 0.16-0.22 the deepened light channels land
     in the same band as dark on their own (line 1.18:1 against 1.08:1, vertex
     1.44:1 against 1.39:1), so scaling back would make light dimmer than dark for
     no reason. The dial stays because the parity is asserted on the OUTCOME, and
     this is the one property that restores it if either palette moves. */
  --mesh-ink: 1;

  --font-sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --header-h: 68px;

  /* ---- Glass header ---------------------------------------------------
     Alpha lives in its own token because the scrolled state deepens it:
     a solid bar would hide the mesh, and a fully transparent one would let
     body copy read through the nav links. 0.52 is the point where the
     animated lattice is still visible behind the bar but text keeps its
     measured contrast. */
  --header-glass-rgb: 12, 13, 19;
  --header-glass-alpha: 0.52;
  --header-glass-alpha-scrolled: 0.64;
  --header-glass-edge-rgb: 255, 255, 255;
  --header-glass-sheen-rgb: 255, 255, 255;
  --header-glass-shadow-rgb: 5, 3, 9;

  --radius: 1.25rem;
  --radius-lg: 1.75rem;

  --shadow-glow: 0 24px 70px -24px rgba(var(--glow-rgb), 0.28);
  --shadow-card: 0 24px 60px -30px rgba(var(--shadow-rgb), 0.9);

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* ---------- 1h. Light theme (follows the system preference) ------------
   The dark theme above is the DEFAULT FALLBACK: nothing here applies unless
   the OS reports `prefers-color-scheme: light`. This block redefines the
   channels rather than the components, so every rgba(var(--x-rgb), a) tint
   in the file follows automatically and the switch is seamless across the
   whole SPA.

   Contrast is measured, not eyeballed. Every text/background pair below
   clears WCAG AA (4.5:1); body copy and the accent-as-text pairs clear AAA.
   The pale violet/lavender of the dark theme is unreadable on white (~1.6:1)
   so those roles are deepened instead of merely inverted.               */
@media (prefers-color-scheme: light) {
  :root {
    color-scheme: light;

    /* ---- channels: deep obsidian -> soft premium off-white ---- */
    --bg-rgb: 247, 249, 252;          /* #F7F9FC */
    --bg-soft-rgb: 250, 250, 252;     /* #FAFAFC */
    --surface-rgb: 255, 255, 255;
    --surface-2-rgb: 241, 244, 249;   /* #F1F4F9 */
    --surface-3-rgb: 231, 235, 242;   /* #E7EBF2 */
    --text-rgb: 26, 30, 35;           /* #1A1E23 rich dark slate */
    --muted-rgb: 75, 85, 99;          /* #4B5563 medium-dark gray, 7.4:1 on --bg */
    --accent-rgb: 109, 40, 217;       /* #6D28D9 violet-800: 6.7:1 as text */
    --accent-deep-rgb: 91, 33, 182;   /* #5B21B6 violet-900: 8.6:1, focus rings */
    --blue-rgb: 29, 78, 216;          /* #1D4ED8 blue-700: 5.6:1 on the darkest surface */
    --lavender-rgb: 109, 40, 217;     /* pale lavender -> deepened to stay legible */
    --violet-soft-rgb: 124, 58, 237;  /* #7C3AED */
    --refuse-rgb: 190, 18, 60;        /* #BE123C rose-700: 5.9:1 */
    --build-rgb: 4, 120, 87;          /* #047857 emerald-700: 5.2:1 */

    /* THE inversion. White-based translucency becomes dark-based, which
       flips every hairline, sheen, grid texture, glass panel and the
       credits-screen floating tags in a single declaration.            */
    --overlay-rgb: 15, 23, 42;

    /* Glows keep a brand hue but at a depth that reads as shadow rather
       than neon; the large card shadows go full slate further down.    */
    --glow-rgb: 109, 40, 217;
    --glow-blue-rgb: 29, 78, 216;
    --shadow-rgb: 15, 23, 42;

    /* ---- surfaces & lines ---- */
    --bg: #F7F9FC;
    --bg-soft: #FAFAFC;
    --surface: #FFFFFF;
    --surface-2: #F1F4F9;
    --surface-3: #E7EBF2;
    --field-bg: #FFFFFF;
    /* Slightly darker than the dark theme's hairline so glass still pops
       against an off-white page instead of disappearing into it.       */
    --border: rgba(15, 23, 42, 0.1);
    --border-strong: rgba(15, 23, 42, 0.18);

    /* ---- type ---- */
    --text: #1A1E23;
    --text-dim: #4B5563;
    /* 5.02:1 on the darkest light surface; still visibly fainter than --text-dim. */
    --text-faint: #5A6472;

    /* The dark mesh values are neon on near-black, so they are deepened the same
       way --lavender and --accent are: same hue, enough ink to read as a line
       rather than a haze. Deepening alone is not enough now that the alphas are
       roughly double what they were -- violet-800 at 0.60 alpha is a solid purple
       dot on off-white -- so --mesh-ink scales the layer back as well. Both moves
       together are what keep the light theme matching the dark one's presence
       instead of its numbers. */
    --mesh-node-rgb: 109, 40, 217;
    --mesh-node-hi-rgb: 91, 33, 182;
    --mesh-glow-rgb: 76, 29, 149;
    --mesh-line-rgb: 109, 40, 217;
    --mesh-ink: 1;
    --text-bright: #0F1216;
    /* The light accent is deep enough to carry white ink, so buttons flip
       from dark-plum-on-violet to white-on-violet (7.2:1).             */
    --on-accent: #FFFFFF;
    --on-gradient: #FFFFFF;

    /* ---- brand & semantics ---- */
    --accent: #6D28D9;
    --accent-deep: #5B21B6;
    --accent-soft: rgba(var(--accent-rgb), 0.1);
    --blue: #1D4ED8;
    --lavender: #6D28D9;
    --violet-soft: #7C3AED;
    /* Deepened so white ink clears AA at EVERY stop, not just the first. */
    --brand-gradient: linear-gradient(90deg, #5B21B6 0%, #6D28D9 48%, #1D4ED8 100%);
    --brand-gradient-ink: linear-gradient(105deg, #5B21B6, #6D28D9 58%, #1D4ED8);
    /* Translucent white over off-white needs less opacity to read as
       glass rather than as flat white paint.                           */
    --glass: rgba(var(--surface-rgb), 0.68);
    --glass-deep: rgba(237, 233, 254, 0.82);

    /* Light glass header: white at a slightly higher alpha than dark, because
       a white veil needs more body to hold its edge over off-white content.
       The edge and sheen channels deepen so the bar still has a visible lip. */
    --header-glass-rgb: 255, 255, 255;
    --header-glass-alpha: 0.56;
    --header-glass-alpha-scrolled: 0.68;
    --header-glass-edge-rgb: 15, 23, 42;
    --header-glass-sheen-rgb: 255, 255, 255;
    --header-glass-shadow-rgb: 15, 23, 42;
    --refuse: #BE123C;
    --refuse-border: rgba(var(--refuse-rgb), 0.3);
    --build: #047857;
    --build-border: rgba(var(--build-rgb), 0.3);

    /* Bright outer bloom -> elegant diffused drop-shadow. */
    --shadow-glow: 0 18px 44px -22px rgba(var(--shadow-rgb), 0.2);
    --shadow-card: 0 18px 40px -24px rgba(var(--shadow-rgb), 0.16);

    /* The ring darkens in light mode so it still reads over the off-white
       mesh instead of glowing white on white. */
    --cursor-lens-edge-rgb: 15, 23, 42;
    --cursor-lens-ring-rgb: 15, 23, 42;
    --cursor-lens-ring-outer-rgb: 255, 255, 255;

    /* Darker chevron: the dark theme's #9aa8bf is only 2.44:1 on white,
       below the 3:1 bar for a non-text control indicator. Matched to
       --text-faint so it reads as part of the field, not as decoration. */
    --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' stroke='%235A6472' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");

    /* The sign/tag rim stays bright: its gradient fill is deep in both
       themes, so a white edge is what keeps the chip reading as glass. */
    --rim-rgb: 255, 255, 255;

    /* Deliberately NOT overridden:
       --device-* : the phone is hardware around dark app screenshots, so a
                    pale bezel would read as broken rather than designed.
       --mask-solid : #000 there is an ALPHA carrier for mask-image, not a
                    theme colour; it must stay opaque in every theme.    */
  }
}

/* ---------- 2. Reset & base ---------- */
*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Belt-and-braces: decorative orbs bleed past the viewport edge and
     must never create a horizontal scrollbar on phones. */
  overflow-x: hidden;
  overflow-x: clip;
}

#why-agora {
  scroll-margin-top: 5rem;
}

/* Set by app.js while the mobile menu is open */
html.menu-open,
html.menu-open body {
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

body {
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.65;
  color: var(--text);
  background: var(--bg);
  background-attachment: fixed;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  overflow-x: hidden;
  overflow-x: clip;
  overflow-wrap: anywhere;
}

/* Ambient layer and controls are created by app.js so the same quiet
   experience follows visitors across every page.

   This is the site's ONLY background canvas. The credits screen used to build a
   second one on top of this, which doubled the frame cost and read as moire
   where the two lattices crossed; that layer is gone and the mesh now covers
   every page identically.

   100vw/100vh rather than 100%: the element is position:fixed, so both resolve
   against the viewport anyway, but the viewport units say it out loud and stay
   correct if a future parent ever gives the canvas an offset parent's box to
   percentage against. The canvas element itself is also sized in px from
   innerWidth/innerHeight in app.js -- these are the CSS box, that is the
   backing store. */
.ambient-mesh {
  position: fixed;
  inset: 0;
  z-index: 0;
  width: 100vw;
  height: 100vh;
  pointer-events: none;
}

/* Deliberately NO mix-blend-mode and NO element opacity, and that is a
   performance decision rather than an aesthetic one.

   A full-viewport `mix-blend-mode` makes the compositor read back and blend the
   entire backdrop group on every single frame, and it defeats GPU layer caching --
   on the credits screen, which already composites two large rotating discs with
   masks and rim gradients over this canvas, it was the most expensive thing the
   background was doing. `opacity: 0.95` then multiplied the whole layer again for
   a 5% dim nobody could see.

   Dropping both is close to free visually, because the two blend modes in use were
   both nearly the identity against these backgrounds. Measured against --bg in
   each theme, at the real 0.20 line / 0.30 vertex alphas:

     dark   line    screen 1.213:1  ->  normal 1.214:1   (max channel delta 2.4/255)
     dark   vertex  screen 1.486:1  ->  normal 1.497:1   (max channel delta 3.6/255)
     light  line  multiply 1.374:1  ->  normal 1.394:1   (max channel delta 1.9/255)
     light  vertex multiply 1.631:1 ->  normal 1.668:1   (max channel delta 2.9/255)

   Screen on near-black and multiply on off-white are both within a few levels of
   plain alpha compositing -- which is exactly why `screen` could hide the light
   theme's mesh at 1.003:1 for so long: it was doing almost nothing in either
   theme. Contrast is very slightly BETTER without it in both, and the numbers the
   test computes now match what is on screen instead of being optimistic by the
   element's 0.95.

   test_the_mesh_is_visible_in_both_themes_without_a_blend_mode recomputes all four
   of those ratios from the tokens on disk, so re-adding a blend mode -- or
   deepening the palette without checking it still reads -- fails there. */

main,
.site-footer,
.slim-footer {
  position: relative;
  z-index: 1;
}

.ambient-toggle {
  position: fixed;
  right: 1.25rem;
  bottom: max(1.25rem, env(safe-area-inset-bottom));
  z-index: 70;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  min-height: 2.35rem;
  padding: 0.55rem 0.8rem;
  border: 1px solid var(--border-strong);
  border-radius: 9999px;
  color: var(--text-dim);
  background: var(--glass);
  -webkit-backdrop-filter: var(--glass-filter);
  backdrop-filter: var(--glass-filter);
  box-shadow: 0 12px 35px -22px rgba(var(--glow-rgb), 0.8);
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.12em;
  line-height: 1;
  text-transform: uppercase;
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease),
    background-color 0.25s var(--ease), box-shadow 0.25s var(--ease);
}

.ambient-toggle svg {
  width: 15px;
  height: 15px;
  flex: none;
  color: var(--accent);
}

.ambient-toggle:hover,
.ambient-toggle:focus-visible,
.ambient-toggle.is-on {
  border-color: rgba(var(--accent-rgb), 0.6);
  color: var(--text);
  background: var(--glass-deep);
  box-shadow: 0 14px 38px -20px rgba(var(--glow-rgb), 0.9);
}

.ambient-toggle.is-on svg {
  color: var(--blue);
}

/* Hidden until the JS enhancement opts in. Coarse pointers and
   reduced-motion visitors keep the native cursor and the full default model. */
.cursor-lens {
  display: none;
  pointer-events: none;
  will-change: transform;
}

.cursor-glow {
  display: none;
  pointer-events: none;
  will-change: transform;
}

/* Zero-sized host for #agora-liquid. It must stay in the document for the
   backdrop-filter reference to resolve, but never paints or hit-tests. */
.cursor-lens-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
  pointer-events: none;
}

@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  body.has-custom-cursor,
  body.has-custom-cursor * {
    cursor: none !important;
  }

  /* The pointer is a LOCAL LIQUID DISTURBANCE, not a piece of glass.

     This 88px disc paints no tint, rim or sheen of its own. It exists only to
     carry a backdrop-filter, which refracts the content under and around the
     pointer. The radial mask holds full strength at the centre then fades to
     transparent by 90% of the radius, so the distortion is strongest around the
     pointer and dissolves smoothly into the page at its outer edge -- a soft
     vignette instead of a hard cut-off.

     The glow below is the only painted part of the field: a low lavender-to-
     blue bloom that lifts the pointer off dark copy without becoming a ring.
     The small solid bead is the visible pointer. Neither element scales,
     rotates or re-letters the content it passes over, so hovering copy can
     never make that copy pop out; only the backdrop bends. */
  body.has-custom-cursor .cursor-lens {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 10000 !important;
    display: block;
    width: 88px;
    height: 88px;
    border: 0;
    border-radius: 50%;
    background: transparent;
    opacity: 0;
    transform: translate3d(-44px, -44px, 0) translate(-50%, -50%);
    -webkit-mask-image: radial-gradient(circle, black 0%, black 30%, transparent 90%);
    mask-image: radial-gradient(circle, black 0%, black 30%, transparent 90%);
    transition: opacity 0.16s var(--ease);
    -webkit-backdrop-filter: blur(8px) saturate(145%);
    backdrop-filter: blur(8px) saturate(145%);
    will-change: transform;
  }

  /* Progressive enhancement: only engines that accept an SVG filter in
     backdrop-filter take the displacement pass. Everything else keeps the
     soft blur above, so the disturbance never disappears. */
  @supports ((backdrop-filter: url(#agora-liquid)) or (-webkit-backdrop-filter: url(#agora-liquid))) {
    body.has-custom-cursor .cursor-lens {
      -webkit-backdrop-filter: url(#agora-liquid) saturate(150%);
      backdrop-filter: url(#agora-liquid) saturate(150%);
    }
  }

  .cursor-lens::before,
  .cursor-lens::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    border-radius: 50%;
    pointer-events: none;
    transform: translate(-50%, -50%);
  }

  /* The core: a solid lavender bead with a crisp white ring and a black outer
     border, exactly like the reference cursor. Nothing fades to transparent --
     it is one filled circle, not an empty ring. */
  .cursor-lens::before {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: rgb(var(--lavender-rgb));
    box-shadow:
      0 0 0 2px rgba(var(--cursor-lens-ring-rgb), 1),
      0 0 0 4px rgba(var(--cursor-lens-ring-outer-rgb), 1);
    transition: width 0.18s var(--ease), height 0.18s var(--ease);
  }

  body.has-custom-cursor .cursor-lens.is-visible {
    opacity: 1;
  }

  /* A background bloom, not a ring: it fades out in every direction, so the
     bead stays the only hard edge. It is sized to the refraction field rather
     than beyond it -- a bloom that outgrows the disturbance reads as a second,
     wider cursor. 72% of this radius plus the blur lands on the lens's own
     40px fall-off. The JS drags it behind the pointer, so this layer is the
     viscous part: the disturbance stays under the pointer while the light
     trails through the medium and settles a beat later. */
  body.has-custom-cursor .cursor-glow {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 9999 !important;
    display: block;
    width: 104px;
    height: 104px;
    border-radius: 50%;
    background: radial-gradient(
      circle,
      rgba(var(--lavender-rgb), 0.32) 0%,
      rgba(var(--violet-soft-rgb), 0.18) 34%,
      rgba(var(--blue-rgb), 0.09) 56%,
      transparent 72%
    );
    opacity: 0;
    transform: translate3d(-52px, -52px, 0) translate(-50%, -50%);
    filter: blur(7px);
    transition: opacity 0.2s var(--ease);
    will-change: transform;
  }

  body.has-custom-cursor .cursor-glow.is-visible {
    opacity: 1;
  }

  body.has-custom-cursor .cursor-lens.is-target::before {
    width: 20px;
    height: 20px;
  }

  body.has-custom-cursor .cursor-lens.is-pressed::before {
    width: 12px;
    height: 12px;
  }
}

/* Belt and braces for touchscreens and motion-sensitive visitors: the JS
   never opts in, and this also clears a stale class if the OS flips. */
@media (hover: none), (pointer: coarse), (prefers-reduced-motion: reduce) {
  body.has-custom-cursor,
  body.has-custom-cursor * {
    cursor: auto !important;
  }

  .cursor-lens {
    display: none !important;
  }

  .cursor-glow {
    display: none !important;
  }
}

@media (max-width: 640px) {
  .ambient-toggle {
    right: 0.85rem;
    bottom: max(0.85rem, env(safe-area-inset-bottom));
    width: 2.35rem;
    min-height: 2.35rem;
    padding: 0;
    justify-content: center;
    gap: 0;
    border-radius: 50%;
  }

  /* On a phone the label is the widest part of this fixed control and the only
     part that can cover a line of body copy. The icon alone keeps the footer,
     the download card and legal text clear; the button's aria-label (kept in
     sync by the sound controller) still announces its state. */
  .ambient-toggle span {
    display: none;
  }
}

img,
svg,
video {
  display: block;
  max-width: 100%;
}

img {
  height: auto;
}

a {
  color: var(--accent);
  text-decoration: none;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
  -webkit-tap-highlight-color: transparent;
}

button {
  cursor: pointer;
  background: none;
  border: 0;
}

ul,
ol {
  padding: 0;
  list-style: none;
}

::selection {
  background: rgba(var(--accent-rgb), 0.28);
  color: var(--text-bright);
}

:focus-visible {
  outline: 2px solid var(--accent-deep);
  outline-offset: 3px;
  border-radius: 4px;
}

/* Skip link (a11y) */
.skip-link {
  position: absolute;
  top: -4rem;
  left: 1rem;
  z-index: 100;
  padding: 0.6rem 1rem;
  border-radius: 0.6rem;
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
  font-size: 0.85rem;
  transition: top 0.2s var(--ease);
}

.skip-link:focus {
  top: 0.75rem;
}

/* ---------- 3. Layout primitives ---------- */
.container {
  width: min(1120px, 100% - 2.5rem);
  margin-inline: auto;
}

@media (min-width: 768px) {
  .container {
    width: min(1120px, 100% - 4rem);
  }
}

.section {
  padding-block: clamp(4rem, 9vw, 7rem);
  position: relative;
}

.section--alt {
  /* Let the site-wide dot mesh remain visible through the alternate band. */
  background: rgba(var(--bg-soft-rgb), 0.62);
  border-block: 1px solid var(--border);
}

.section-head {
  max-width: 46rem;
  margin-bottom: clamp(2.25rem, 5vw, 3.5rem);
}

.section-head--center {
  margin-inline: auto;
  text-align: center;
}

.section-head p {
  margin-top: 1rem;
  color: var(--text-dim);
}

/* Mono eyebrow labels — the "signal" motif */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--accent);
  opacity: 0.85;
}

.eyebrow::before {
  content: "";
  width: 1.9rem;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--accent-deep));
}

.eyebrow--center::after {
  content: "";
  width: 1.9rem;
  height: 1px;
  background: linear-gradient(90deg, var(--accent-deep), transparent);
}

.eyebrow__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 12px rgba(var(--glow-rgb), 0.9);
  animation: pulse 2.6s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.45; transform: scale(0.8); }
}

@media (prefers-reduced-motion: reduce) {
  .eyebrow__dot {
    animation: none;
  }
}

/* Type scale */
h1, h2, h3 {
  line-height: 1.08;
  letter-spacing: -0.025em;
  font-weight: 650;
  text-wrap: balance;
}

.display {
  font-size: clamp(2.6rem, 8vw, 5rem);
  line-height: 1.02;
  letter-spacing: -0.035em;
  font-weight: 700;
}

.h2 {
  font-size: clamp(1.8rem, 4.5vw, 2.9rem);
}

.h3 {
  font-size: 1.2rem;
  letter-spacing: -0.015em;
}

.accent {
  color: var(--accent);
}

.lead {
  color: var(--text-dim);
  font-size: 1.05rem;
  line-height: 1.8;
}

.mono {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* ---------- 4. Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 0.95rem 1.7rem;
  border-radius: 9999px;
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1;
  border: 1px solid transparent;
  transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease),
    background-color 0.25s var(--ease), border-color 0.25s var(--ease),
    color 0.25s var(--ease);
  white-space: nowrap;
}

.btn:active {
  transform: translateY(1px) scale(0.99);
}

.btn svg {
  flex: none;
}

/* All filled actions share the same dark label and violet-to-blue brand
   gradient at every button size, including on hover. */
.btn--primary {
  border-radius: 9999px;
  /* Keep the accent as the fallback while the violet-to-blue brand gradient
     gives the actions and wordmark a shared visual signal. */
  background: var(--accent);
  background-image: var(--brand-gradient);
  color: var(--on-accent);
  box-shadow: 0 0 0 0 rgba(var(--glow-rgb), 0);
}

.btn--primary:hover {
  background: var(--accent);
  background-image: var(--brand-gradient);
  filter: saturate(1.08) brightness(1.04);
  transform: translateY(-2px);
  box-shadow: 0 14px 40px -12px rgba(var(--glow-rgb), 0.45);
}

.btn--ghost {
  border-color: var(--border-strong);
  color: var(--text);
  background: rgba(var(--overlay-rgb), 0.04);
}

.btn--ghost:hover {
  border-color: rgba(var(--accent-rgb), 0.55);
  color: var(--accent);
  background: var(--accent-soft);
  transform: translateY(-2px);
}

.btn--lg {
  padding: 1.15rem 2.2rem;
  font-size: 1.02rem;
}

.btn--sm {
  padding: 0.6rem 1.1rem;
  font-size: 0.8rem;
}

.btn--block {
  width: 100%;
}

.btn[disabled] {
  opacity: 0.55;
  cursor: not-allowed;
  transform: none;
}

/* ---------- 5. Header / nav ---------- */
/* True glass: the animated mesh and the first pixels of body copy stay
   visible through the bar instead of being painted over by an opaque strip.
   20px is the point where scrolling text turns into soft colour without
   smearing the nav labels themselves. */
.site-header {
  position: fixed;
  inset-inline: 0;
  top: 0;
  z-index: 60;
  /* The nav inside is also --header-h tall. Without an explicit height on the
     bar itself, its 1px border sat OUTSIDE that box and the site header
     measured 69px while the credits header measured 68px -- a one-pixel jump
     on every PJAX swap. With border-box, both are exactly --header-h. */
  height: var(--header-h);
  background: rgba(var(--header-glass-rgb), var(--header-glass-alpha));
  -webkit-backdrop-filter: var(--glass-filter);
  backdrop-filter: var(--glass-filter);
  border-bottom: 1px solid rgba(var(--header-glass-edge-rgb), 0.14);
  box-shadow:
    inset 0 1px 0 rgba(var(--header-glass-sheen-rgb), 0.18),
    inset 0 -1px 0 rgba(var(--header-glass-edge-rgb), 0.05),
    0 18px 44px -34px rgba(var(--header-glass-shadow-rgb), 0.65);
  transition: border-color 0.3s var(--ease), background-color 0.3s var(--ease),
    box-shadow 0.3s var(--ease);
}

/* A single specular hairline across the top edge: the highlight that makes a
   glass pane read as a pane rather than as a tinted rectangle. */
.site-header::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent,
    rgba(var(--header-glass-sheen-rgb), 0.38) 28%,
    rgba(var(--header-glass-sheen-rgb), 0.2) 72%,
    transparent
  );
  pointer-events: none;
}

.site-header.is-scrolled {
  border-bottom-color: rgba(var(--header-glass-edge-rgb), 0.22);
  background: rgba(var(--header-glass-rgb), var(--header-glass-alpha-scrolled));
  box-shadow:
    inset 0 1px 0 rgba(var(--header-glass-sheen-rgb), 0.24),
    inset 0 -1px 0 rgba(var(--header-glass-edge-rgb), 0.08),
    0 22px 50px -32px rgba(var(--header-glass-shadow-rgb), 0.78);
}

.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  /* Fill the header's content box rather than re-declaring its height. The bar
     is exactly --header-h including its border; a hard 68px nav inside a 67px
     content box overflowed by the width of that border. */
  height: 100%;
}

.nav__logo {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  flex: none;
}

.nav__logo-mark {
  width: 34px;
  height: 34px;
  object-fit: contain;
  flex: none;
}

/* Brand lockup — the app's lowercase wordmark, with its violet period. */
.nav__logo-name {
  display: inline-flex;
  align-items: baseline;
  overflow: visible;
  font-family: var(--font-sans);
  font-size: 1.35rem;
  font-weight: 700;
  letter-spacing: -0.065em;
  line-height: 1.15;
  color: var(--text);
  white-space: nowrap;
  transition: color 0.25s var(--ease);
}

.nav__logo-word {
  display: inline-block;
  padding-bottom: 0.08em;
  text-transform: lowercase;
  line-height: 1.15;
  /* Keep the first letters at full brightness, then let the wordmark settle
     into roughly 74% visibility toward the right edge. */
  color: var(--text); /* solid fallback for browsers without text clipping */
  background-image: linear-gradient(
    90deg,
    var(--text) 0%,
    var(--text) 22%,
    rgba(var(--text-rgb), 0.9) 58%,
    rgba(var(--text-rgb), 0.74) 100%
  );
  background-repeat: no-repeat;
  background-size: 100% 100%;
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.nav__logo-dot {
  display: inline-block;
  margin-left: 0.035em;
  color: var(--accent); /* solid fallback for browsers without text clipping */
  background-image: var(--brand-gradient);
  background-size: 180% 100%;
  background-position: left center;
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  font-size: 1.18em;
  line-height: 0.75;
  filter: drop-shadow(0 0 7px rgba(var(--glow-rgb), 0.38));
  transition: background-position 0.35s var(--ease), filter 0.25s var(--ease);
}

.nav__logo:hover .nav__logo-name,
.nav__logo:focus-visible .nav__logo-name {
  color: var(--text);
}

.nav__logo:hover .nav__logo-dot,
.nav__logo:focus-visible .nav__logo-dot {
  background-position: right center;
  filter: drop-shadow(0 0 9px rgba(var(--glow-blue-rgb), 0.52));
}

.nav__logo--footer .nav__logo-name {
  font-size: 1.4rem;
}

/* Compact variant — keep the same app wordmark in the slim footer. */
.nav__logo--compact {
  gap: 0.45rem;
}

.nav__logo--compact .nav__logo-mark {
  width: 24px;
  height: 24px;
}

.nav__logo--compact .nav__logo-name {
  font-family: var(--font-sans);
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: -0.06em;
  text-transform: none;
  color: var(--text);
}

.nav__logo--compact .nav__logo-dot {
  font-size: 1.1em;
}

.nav__links {
  display: none;
  align-items: center;
  gap: 2rem;
}

.nav__link {
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-dim);
  padding: 0.4rem 0;
  position: relative;
  transition: color 0.2s var(--ease);
}

.nav__link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.3s var(--ease);
}

.nav__link:hover {
  color: var(--text);
}

.nav__link.is-active {
  color: var(--accent);
}

.nav__link.is-active::after,
.nav__link:hover::after {
  transform: scaleX(1);
}

.nav__actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.nav__cta {
  display: none;
}

@media (min-width: 769px) {
  .nav__cta {
    display: inline-flex;
  }
}

/* Hamburger */
.nav__toggle {
  display: inline-flex;
  flex-direction: column;
  justify-content: center;
  gap: 6px;
  width: 44px;
  height: 44px;
  align-items: center;
  border-radius: 0.75rem;
  border: 1px solid var(--border);
  background: rgba(var(--overlay-rgb), 0.05);
}

.nav__toggle-bar {
  display: block;
  width: 18px;
  height: 2px;
  border-radius: 2px;
  background: var(--text);
  transition: transform 0.3s var(--ease), opacity 0.2s var(--ease);
}

.nav__toggle.is-open .nav__toggle-bar:first-child {
  transform: translateY(4px) rotate(45deg);
}

.nav__toggle.is-open .nav__toggle-bar:last-child {
  transform: translateY(-4px) rotate(-45deg);
}

/* Mobile menu */
.mobile-menu {
  position: fixed;
  top: var(--header-h);
  inset-inline: 0;
  z-index: 55;
  display: grid;
  gap: 0.1rem;
  padding: 0.9rem 1.25rem 1.6rem;
  max-height: calc(100vh - var(--header-h));
  max-height: calc(100dvh - var(--header-h));
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* A full-screen nav cannot show the page through it without ghosting the
     hero copy, so it stays nearly opaque; the shared frost still applies to
     the sliver of backdrop that reaches it. */
  background: rgba(var(--bg-rgb), 0.98);
  border-bottom: 1px solid var(--border);
  opacity: 0;
  transform: translateY(-12px);
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.25s var(--ease), transform 0.25s var(--ease), visibility 0.25s;
}

.mobile-menu.is-open {
  opacity: 1;
  transform: none;
  visibility: visible;
  pointer-events: auto;
}

.mobile-menu a:not(.btn) {
  padding: 0.95rem 0.35rem;
  font-size: 1.02rem;
  font-weight: 500;
  color: var(--text);
  border-bottom: 1px solid rgba(var(--overlay-rgb), 0.08);
}

.mobile-menu a:not(.btn):active {
  color: var(--accent);
}

.mobile-menu .btn {
  margin-top: 1.1rem;
}

@media (min-width: 769px) {
  .nav__links {
    display: flex;
  }
  .nav__toggle,
  .mobile-menu {
    display: none;
  }
}

.shared .nav__cta {
  display: inline-flex;
}

/* The slim shared-link header has no hamburger, so the lockup, the secondary
   link and the CTA all share one row. On phones the secondary link yields
   first: the header must never overflow horizontally (the body clips it). */
@media (max-width: 480px) {
  .shared .nav__link {
    display: none;
  }
}

/* ---------- 6. Hero (home) ---------- */
.hero {
  position: relative;
  padding-top: calc(var(--header-h) + clamp(3rem, 9vw, 6rem));
  padding-bottom: clamp(3.5rem, 8vw, 6rem);
  overflow: hidden;
}

.hero__grid {
  display: grid;
  gap: 3.25rem;
  align-items: center;
}

@media (min-width: 1024px) {
  .hero__grid {
    grid-template-columns: 1.12fr 0.88fr;
    gap: 2rem;
  }
}

.hero .display {
  margin-top: 1.4rem;
}

.hero__lead {
  max-width: 33rem;
  margin-top: 1.4rem;
  color: var(--text-dim);
  font-size: 1.06rem;
  line-height: 1.8;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.85rem;
  margin-top: 2.1rem;
}

.hero__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.4rem;
  margin-top: 2.6rem;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.hero__meta span {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
}

.hero__meta span::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
  opacity: 0.8;
}

/* Decorative orbs */
.orb {
  position: absolute;
  border: 1px solid rgba(var(--accent-rgb), 0.09);
  border-radius: 50%;
  pointer-events: none;
}

.orb--one {
  width: 30rem;
  height: 30rem;
  right: -12rem;
  top: 12%;
}

.orb--two {
  width: 21rem;
  height: 21rem;
  right: -8rem;
  top: 22%;
  border-color: rgba(var(--accent-rgb), 0.08);
}

/* ---------- 7. Phone frame (real app screenshots) ----------
   A single reusable bezel. Drop any 9:16 portrait screenshot into
   .phone__screen > img. Taller captures are contained, never cropped.

   Sizes:  .phone        default (gallery)        ~ 260px wide
           .phone--hero  hero visual              ~ 320px wide
           .phone--sm    compact (cards, shared)  ~ 200px wide
   ------------------------------------------------------------ */
.phone {
  --phone-w: 260px;
  --bezel: 0.55rem;
  --phone-radius: 2.4rem;
  position: relative;
  width: var(--phone-w);
  max-width: 100%;
  margin-inline: auto;
  padding: var(--bezel);
  border-radius: var(--phone-radius);
  background:
    linear-gradient(160deg, rgba(var(--device-ring-rgb), 0.09), rgba(var(--device-ring-rgb), 0) 45%),
    linear-gradient(165deg, var(--device-hi), var(--device-mid) 60%, var(--device-lo));
  border: 1px solid rgba(var(--device-ring-rgb), 0.16);
  box-shadow:
    inset 0 0 0 1px rgba(var(--device-ring-rgb), 0.04),
    0 1px 0 rgba(var(--device-ring-rgb), 0.06),
    var(--shadow-glow),
    var(--shadow-card);
  transition: transform 0.6s var(--ease), box-shadow 0.6s var(--ease);
}

.phone__screen {
  position: relative;
  aspect-ratio: 9 / 16;
  overflow: hidden;
  border-radius: calc(var(--phone-radius) - var(--bezel));
  background: var(--device-screen);
  /* faint inner ring so a dark screenshot still reads as a screen */
  box-shadow: inset 0 0 0 1px rgba(var(--device-ring-rgb), 0.12);
}

.phone__screen img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: top center;
}

/* Missing launch captures preserve the bezel without fake app imagery.
   The pending message disappears automatically when a real image loads. */
.phone__screen img {
  opacity: 0;
}

.phone__screen.is-ready img {
  opacity: 1;
}

.phone__pending {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 1rem;
  text-align: center;
  color: var(--device-ink);
  font-size: 0.85rem;
  background: radial-gradient(ellipse at top, var(--device-accent-soft), transparent 70%);
}

.phone__pending small {
  margin-top: 0.5rem;
  color: var(--device-ink-dim);
  font-size: 0.7rem;
}

.phone__screen.is-ready .phone__pending {
  display: none;
}

/* Side buttons */
.phone::before,
.phone::after {
  content: "";
  position: absolute;
  width: 2px;
  border-radius: 2px;
  /* Side buttons live on the bezel, which stays dark in both themes. */
  background: rgba(var(--device-ring-rgb), 0.35);
}

.phone::before {
  right: -3px;
  top: 22%;
  height: 12%;
}

.phone::after {
  left: -3px;
  top: 26%;
  height: 7%;
}

.phone--hero {
  --phone-w: 320px;
  --bezel: 0.6rem;
  --phone-radius: 2.7rem;
}

.phone--sm {
  --phone-w: 200px;
  --bezel: 0.45rem;
  --phone-radius: 1.9rem;
}

/* Hero placement */
.hero__visual {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 1.1rem;
  margin-top: 0.5rem;
}

.hero__visual::before {
  content: "";
  position: absolute;
  inset: -10% -18%;
  background: radial-gradient(closest-side, rgba(var(--accent-rgb), 0.14), transparent 72%);
  filter: blur(12px);
  pointer-events: none;
}

.hero__visual .phone {
  transform: rotate(-2.5deg);
}

.hero__visual:hover .phone {
  transform: rotate(0deg) translateY(-6px);
}

.hero__visual-caption {
  position: relative;
}

@media (prefers-reduced-motion: reduce) {
  .hero__visual .phone,
  .hero__visual:hover .phone {
    transform: none;
  }
}

/* Screenshot gallery (home) */
.screens {
  display: grid;
  gap: 2.5rem 1.5rem;
  justify-items: center;
}

@media (min-width: 640px) {
  .screens {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1024px) {
  .screens {
    grid-template-columns: repeat(3, 1fr);
    gap: 2rem;
  }
}

.screen {
  display: grid;
  gap: 1.25rem;
  width: 100%;
  max-width: 320px;
  text-align: center;
}

.screen:hover .phone {
  transform: translateY(-6px);
  box-shadow:
    inset 0 0 0 1px rgba(var(--device-ring-rgb), 0.04),
    0 30px 80px -28px rgba(var(--glow-rgb), 0.4),
    var(--shadow-card);
}

.screen figcaption strong {
  display: block;
  font-size: 1.02rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.screen figcaption span {
  display: block;
  margin-top: 0.35rem;
  font-size: 0.9rem;
  line-height: 1.65;
  color: var(--text-dim);
}

/* In-app peek (shared-post page) */
.peek {
  display: grid;
  gap: 1.75rem;
  justify-items: center;
  text-align: center;
  padding: clamp(1.6rem, 5vw, 2.4rem);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background:
    radial-gradient(28rem 14rem at 0% 100%, rgba(var(--accent-rgb), 0.16), transparent 60%),
    var(--surface);
}

@media (min-width: 600px) {
  .peek {
    grid-template-columns: 200px minmax(0, 1fr);
    align-items: center;
    text-align: left;
    gap: 2.25rem;
  }
}

.peek__text h2 {
  margin-top: 0.9rem;
  font-size: clamp(1.3rem, 3.4vw, 1.7rem);
}

.peek__text p:last-child {
  margin-top: 0.8rem;
  font-size: 0.93rem;
  line-height: 1.75;
  color: var(--text-dim);
}

/* Download card visual */
.dl-card__visual {
  display: none;
}

/* ---------- 8. Principle cards (motive) ---------- */
.principles {
  display: grid;
  gap: 1.1rem;
}

@media (min-width: 720px) {
  .principles {
    grid-template-columns: repeat(2, 1fr);
    gap: 1.4rem;
  }
}

.principle {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  padding: 1.9rem 1.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: linear-gradient(160deg, var(--glass), rgba(var(--surface-rgb), 0.34));
  transition: transform 0.35s var(--ease), border-color 0.35s var(--ease), box-shadow 0.35s var(--ease);
  overflow: hidden;
}

.principle::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(22rem 12rem at 85% -10%, rgba(var(--accent-rgb), 0.09), transparent 60%);
  opacity: 0;
  transition: opacity 0.4s var(--ease);
}

.principle:hover {
  transform: translateY(-5px);
  border-color: rgba(var(--accent-rgb), 0.35);
  box-shadow: 0 30px 60px -34px rgba(var(--glow-rgb), 0.35);
}

.principle:hover::before {
  opacity: 1;
}

.principle__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
}

.principle__icon {
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border-radius: 12px;
  border: 1px solid rgba(var(--accent-rgb), 0.28);
  background: var(--accent-soft);
  color: var(--accent);
}

.principle__icon svg {
  width: 22px;
  height: 22px;
}

.principle__num {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.2em;
  color: var(--text-faint);
}

.principle h3 {
  font-size: 1.28rem;
  letter-spacing: -0.02em;
}

.principle p {
  color: var(--text-dim);
  font-size: 0.95rem;
  line-height: 1.75;
}

/* ---------- 9. Quote band ---------- */
.quote {
  position: relative;
  overflow: hidden;
  text-align: center;
}

.quote blockquote {
  max-width: 56rem;
  margin-inline: auto;
  font-size: clamp(1.7rem, 4.6vw, 3rem);
  line-height: 1.22;
  letter-spacing: -0.03em;
  font-weight: 600;
  color: var(--text);
  text-wrap: balance;
}

.quote cite {
  display: block;
  margin-top: 1.6rem;
  font-style: normal;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--accent);
  opacity: 0.85;
}

.quote .orb--one {
  right: auto;
  left: -14rem;
  top: -10rem;
  border-color: rgba(var(--accent-rgb), 0.2);
}

/* ---------- 10. CTA panel ---------- */
/* The closing invitation on about.html. A hairline band rather than a glass panel,
   because the section immediately after it IS a .cta-panel and two of those in a row
   read as a duplicated block. Same tokens, different register. */
.contact-callout {
  padding: clamp(2rem, 5vw, 3.25rem) clamp(1.5rem, 4vw, 3rem);
  border: 1px solid var(--border);
  border-top: 2px solid rgba(var(--accent-rgb), 0.45);
  border-radius: var(--radius);
  background: linear-gradient(180deg, rgba(var(--accent-rgb), 0.05), transparent 62%);
  text-align: center;
}

.contact-callout h2 {
  margin-bottom: 0.8rem;
}

.contact-callout p {
  max-width: 46rem;
  margin: 0 auto 1.6rem;
  color: var(--text-dim);
}

.cta-panel {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  padding: clamp(2.4rem, 6vw, 4.5rem);
  background:
    radial-gradient(46rem 20rem at 88% -30%, rgba(var(--accent-rgb), 0.16), transparent 55%),
    radial-gradient(38rem 18rem at -5% 130%, rgba(var(--accent-rgb), 0.26), transparent 55%),
    var(--surface);
  box-shadow: var(--shadow-card);
}

.cta-panel h2 {
  font-size: clamp(2rem, 5.5vw, 3.4rem);
  letter-spacing: -0.035em;
  max-width: 34rem;
}

.cta-panel p {
  margin-top: 1rem;
  color: var(--text-dim);
  max-width: 32rem;
}

.cta-panel .hero__actions {
  margin-top: 2rem;
}

.cta-panel .hero__meta {
  margin-top: 2.2rem;
}

/* ---------- 11. Page hero (inner pages) ---------- */
.page-hero {
  position: relative;
  padding-top: calc(var(--header-h) + clamp(3rem, 9vw, 5.5rem));
  padding-bottom: clamp(2.75rem, 7vw, 4.5rem);
  overflow: hidden;
}

.page-hero .display {
  margin-top: 1.4rem;
  max-width: 20ch;
}

.page-hero .lead {
  max-width: 36rem;
  margin-top: 1.4rem;
}

.page-hero .orb--one {
  right: -10rem;
  top: 4%;
}

/* ---------- 12. Download card ---------- */
.dl-card {
  display: grid;
  gap: 2rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  padding: clamp(1.9rem, 5vw, 3.25rem);
  background:
    radial-gradient(40rem 18rem at 100% 0%, rgba(var(--accent-rgb), 0.08), transparent 55%),
    var(--surface);
  box-shadow: var(--shadow-card);
}

@media (min-width: 880px) {
  .dl-card {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
    align-items: center;
    gap: 3rem;
  }
}

@media (min-width: 1080px) {
  .dl-card {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr) 190px;
    column-gap: 2.5rem;
  }
  .dl-card__visual {
    --phone-w: 190px;
    display: block;
    margin-inline: 0;
    justify-self: end;
  }
}

.dl-card__icon {
  width: 56px;
  height: 56px;
  object-fit: contain;
}

.dl-card h2 {
  margin-top: 1.25rem;
  font-size: clamp(1.5rem, 3.4vw, 2.1rem);
}

.dl-card__desc {
  margin-top: 0.8rem;
  color: var(--text-dim);
  font-size: 0.97rem;
}

.badges {
  display: flex;
  flex-wrap: wrap;
  gap: 0.55rem;
  margin-top: 1.4rem;
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.42rem 0.85rem;
  border-radius: 9999px;
  border: 1px solid var(--border);
  background: rgba(var(--overlay-rgb), 0.05);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.badge--accent {
  border-color: rgba(var(--accent-rgb), 0.4);
  background: var(--accent-soft);
  color: var(--accent);
}

.badge--subtle {
  border-color: rgba(var(--accent-rgb), 0.4);
  background: rgba(var(--accent-rgb), 0.06);
  color: var(--accent);
}

.dl-card__actions {
  display: grid;
  gap: 0.9rem;
  justify-items: stretch;
}

.dl-card__note {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
  text-align: center;
}

/* ---------- 13. Specs / requirements ---------- */
.specs {
  display: grid;
  gap: 2.5rem;
}

@media (min-width: 880px) {
  .specs {
    grid-template-columns: 1fr 1fr;
    gap: 4rem;
  }
}

.specs h3 {
  font-size: 1.1rem;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  color: var(--text);
}

.specs h3 svg {
  width: 18px;
  height: 18px;
  color: var(--accent);
}

.spec-list {
  margin-top: 1.4rem;
  border-top: 1px solid var(--border);
}

.spec-list li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1.25rem;
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.95rem;
}

.spec-list .k {
  color: var(--text-faint);
}

.spec-list .v {
  text-align: right;
  font-weight: 500;
}

.spec-list .v small {
  display: block;
  color: var(--text-faint);
  font-weight: 400;
  font-size: 0.78rem;
}

/* ---------- 14. Steps (install guide) ---------- */
.steps {
  display: grid;
  gap: 1.25rem;
  counter-reset: step;
}

@media (min-width: 820px) {
  .steps {
    grid-template-columns: repeat(3, 1fr);
    gap: 1.4rem;
  }
}

.step {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  padding: 1.8rem 1.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  transition: transform 0.35s var(--ease), border-color 0.35s var(--ease);
}

.step:hover {
  transform: translateY(-4px);
  border-color: rgba(var(--accent-rgb), 0.3);
}

.step__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.step__num {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.24em;
  color: var(--accent);
}

.step__icon {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: rgba(var(--overlay-rgb), 0.06);
  color: var(--text-dim);
}

.step__icon svg {
  width: 20px;
  height: 20px;
}

.step h3 {
  font-size: 1.08rem;
}

.step p {
  font-size: 0.92rem;
  line-height: 1.7;
  color: var(--text-dim);
}

.step code {
  font-family: var(--font-mono);
  font-size: 0.8em;
  padding: 0.1em 0.45em;
  border-radius: 0.4em;
  border: 1px solid var(--border);
  background: rgba(var(--overlay-rgb), 0.08);
  color: var(--accent);
  white-space: nowrap;
}

@media (max-width: 480px) {
  .step code {
    white-space: normal;
  }
}

/* ---------- 15. Shared-post page ---------- */
.shared {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.shared__main {
  flex: 1;
  display: flex;
  justify-content: center;
  padding: calc(var(--header-h) + clamp(2.5rem, 7vw, 4.5rem)) 0 clamp(3rem, 7vw, 5rem);
}

.shared__inner {
  width: min(680px, 100% - 2rem);
  display: grid;
  gap: 1.6rem;
}

.shared-card {
  text-align: center;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  padding: clamp(2.2rem, 6vw, 3.4rem) clamp(1.5rem, 5vw, 3rem);
  background:
    radial-gradient(30rem 16rem at 50% -30%, rgba(var(--accent-rgb), 0.12), transparent 60%),
    var(--surface);
  box-shadow: var(--shadow-card);
}

.shared-card__icon {
  display: grid;
  place-items: center;
  width: 64px;
  height: 64px;
  margin-inline: auto;
  border-radius: 50%;
  border: 1px solid rgba(var(--accent-rgb), 0.4);
  background: var(--accent-soft);
  color: var(--accent);
  box-shadow: 0 0 40px -6px rgba(var(--glow-rgb), 0.35);
}

.shared-card__icon svg {
  width: 28px;
  height: 28px;
}

.shared-card h1 {
  margin-top: 1.5rem;
  font-size: clamp(1.45rem, 4vw, 2.05rem);
  letter-spacing: -0.02em;
  line-height: 1.3;
  text-wrap: balance;
}

.shared-card > p {
  margin-top: 0.9rem;
  color: var(--text-dim);
  max-width: 30rem;
  margin-inline: auto;
}

.shared-card .btn-row {
  display: grid;
  gap: 0.8rem;
  margin-top: 1.9rem;
}

@media (min-width: 520px) {
  .shared-card .btn-row {
    grid-template-columns: 1fr 1fr;
  }
}

.shared-card .btn-row .btn--primary {
  grid-column: 1 / -1;
}

/* "Already have the app?" — a secondary row under the hero CTAs for the visitor
   who landed here with Agora already installed. Kept visibly quieter than the
   row above it: most people who reach this page do NOT have the app, so this
   must never compete with "Get the app". */
.shared-card__have-app {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
  margin-top: 1.1rem;
  font-size: 0.86rem;
  color: var(--text-dim);
}

/* Post preview panel */
.post-preview {
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: rgba(var(--surface-rgb), 0.65);
  padding: 1.5rem 1.5rem 1.6rem;
  text-align: left;
}

.post-preview__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.post-preview__label {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.35rem 0.7rem;
  border-radius: 9999px;
  border: 1px solid rgba(var(--accent-rgb), 0.35);
  background: var(--accent-soft);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  color: var(--accent);
}

.chip--idle {
  border-color: var(--border);
  background: transparent;
  color: var(--text-faint);
}

.post-preview__body {
  margin-top: 1.25rem;
}

.post-preview__head {
  display: flex;
  align-items: center;
  gap: 0.7rem;
}

.post-preview__author {
  font-size: 0.92rem;
  font-weight: 600;
}

/* Author card. The avatar is a third-party URL, so the frame always paints a
   tinted initial underneath it: a slow, blocked or deleted image degrades to
   the monogram rather than a broken-image glyph. */
.post-preview__avatar {
  position: relative;
  flex: none;
  width: 3.5rem;
  height: 3.5rem;
  border-radius: 50%;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-soft);
  border: 1px solid rgba(var(--accent-rgb), 0.28);
}

.post-preview__monogram {
  font-size: 1.15rem;
  font-weight: 600;
  line-height: 1;
  color: var(--accent);
  user-select: none;
}

.post-preview__avatar-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Only hide the initial once the image has really painted — a transparent
   avatar would otherwise let it show through behind the member's face. */
.post-preview__avatar.has-avatar .post-preview__monogram {
  visibility: hidden;
}

.post-preview__who {
  min-width: 0;
}

.post-preview__handle {
  margin-top: 0.15rem;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  color: var(--text-faint);
  overflow-wrap: anywhere;
}

.post-preview__posted {
  margin-top: 0.3rem;
  font-size: 0.8rem;
  color: var(--text-dim);
}

.post-preview__foot {
  margin-top: 1.3rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--border);
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  font-size: 0.82rem;
  line-height: 1.6;
  color: var(--text-dim);
}

.post-preview__foot svg {
  width: 16px;
  height: 16px;
  flex: none;
  margin-top: 0.15rem;
  color: var(--accent);
}

.post-preview__empty {
  margin-top: 1.25rem;
  font-size: 0.9rem;
  color: var(--text-dim);
  line-height: 1.7;
}

.post-preview__empty code {
  font-family: var(--font-mono);
  font-size: 0.85em;
  color: var(--accent);
}

/* "Open in the app" row — revealed only where intent:// resolves. */

/* Security note */
.secure-note {
  display: flex;
  align-items: flex-start;
  gap: 0.8rem;
  padding: 1.1rem 1.3rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: rgba(var(--accent-rgb), 0.04);
  font-size: 0.85rem;
  line-height: 1.65;
  color: var(--text-dim);
}

.secure-note svg {
  width: 18px;
  height: 18px;
  flex: none;
  margin-top: 0.1rem;
  color: var(--accent);
}

.secure-note strong {
  color: var(--text);
}

/* Slim footer for the shared page */
.slim-footer {
  border-top: 1px solid var(--border);
  padding: 1.4rem 0;
}

.slim-footer .container {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 1.5rem;
  align-items: center;
  justify-content: space-between;
  font-family: var(--font-mono);
  font-size: 0.64rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.slim-footer a {
  color: var(--text-dim);
}

.slim-footer a:hover {
  color: var(--accent);
}

/* ---------- 16. About page ---------- */
.vision-grid {
  display: grid;
  gap: 2.5rem;
}

@media (min-width: 900px) {
  .vision-grid {
    grid-template-columns: 0.9fr 1.1fr;
    gap: 4.5rem;
    align-items: start;
  }
  .vision-grid .section-head {
    position: sticky;
    top: calc(var(--header-h) + 2rem);
  }
}

.vision-body p {
  color: var(--text-dim);
  margin-bottom: 1.2rem;
  line-height: 1.85;
}

.vision-body p strong {
  color: var(--text);
  font-weight: 600;
}

.standards {
  display: grid;
  gap: 1.25rem;
  margin-top: 2.5rem;
}

@media (min-width: 720px) {
  .standards {
    grid-template-columns: 1fr 1fr;
    gap: 1.4rem;
  }
}

.standard {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.6rem 1.5rem;
  background: var(--surface);
}

.standard h3 {
  font-size: 0.8rem;
  font-weight: 600;
  font-family: var(--font-mono);
  letter-spacing: 0.22em;
  text-transform: uppercase;
}

/* Semantic color is scoped to the values cards, never the main CTAs. */
.standard--refuse {
  border-color: var(--refuse-border);
}

.standard--build {
  border-color: var(--build-border);
}

.standard h3 {
  display: inline-block;
  padding: 0.5rem 0.65rem;
  border-radius: 0.65rem;
  border: 1px solid;
  line-height: 1.5;
}

.standard--refuse h3 {
  color: var(--refuse);
  background: rgba(var(--refuse-rgb), 0.06);
  border-color: var(--refuse-border);
}

.standard--build h3 {
  color: var(--build);
  background: rgba(var(--build-rgb), 0.06);
  border-color: var(--build-border);
}

.standard li {
  position: relative;
  padding: 0.6rem 0 0.6rem 1.7rem;
  font-size: 0.93rem;
  line-height: 1.6;
  color: var(--text-dim);
  border-bottom: 1px solid rgba(var(--overlay-rgb), 0.07);
}

.standard li:last-child {
  border-bottom: 0;
}

.standard li::before {
  content: "";
  position: absolute;
  left: 0.2rem;
  top: 1.15rem;
  width: 0.55rem;
  height: 2px;
  border-radius: 2px;
}

.standard--refuse li::before {
  background: var(--refuse);
  transform: rotate(45deg);
  width: 0.6rem;
  height: 2px;
  border-radius: 2px;
}

.standard--build li::before {
  background: var(--build);
}

/* Creator card */
.creator {
  display: grid;
  gap: 2.25rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  padding: clamp(1.9rem, 5vw, 3.25rem);
  background:
    radial-gradient(36rem 18rem at 0% 0%, rgba(var(--accent-rgb), 0.16), transparent 55%),
    var(--surface);
  box-shadow: var(--shadow-card);
}

@media (min-width: 880px) {
  .creator {
    grid-template-columns: 18rem 1fr;
    gap: 3.5rem;
  }
}

.creator__card {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 2rem 1.6rem;
  text-align: center;
  background: var(--glass);
}

.creator__name {
  margin-top: 1.1rem;
  font-size: 1.15rem;
  font-weight: 650;
}

.creator__role {
  margin-top: 0.3rem;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent);
}

/* Reserve the real portrait's space; never substitute a made-up person. */
.developer-portrait {
  position: relative;
  width: 160px;
  height: 160px;
  max-width: 100%;
  aspect-ratio: 1;
  margin-inline: auto;
  border-radius: 50%;
  background: var(--surface);
  box-shadow: 0 0 0 1px rgba(var(--glow-rgb), 0.3), 0 0 32px rgba(var(--glow-rgb), 0.12);
}

.developer-avatar {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  border: 3px solid var(--surface);
  object-fit: cover;
  opacity: 0;
}

.developer-portrait__pending {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 1rem;
  color: var(--text-dim);
  font-size: 0.8rem;
}

.developer-portrait.is-ready .developer-avatar {
  opacity: 1;
}

.developer-portrait.is-ready .developer-portrait__pending {
  display: none;
}

.creator__bio p {
  color: var(--text-dim);
  line-height: 1.85;
  margin-bottom: 1.1rem;
}

.creator__bio p strong {
  color: var(--text);
  font-weight: 600;
}

.creator__bio .highlight {
  border-left: 2px solid var(--accent-deep);
  padding-left: 1.1rem;
  margin: 1.6rem 0;
  font-size: 1.02rem;
  color: var(--text);
}

/* ---------- 17. Contact form ---------- */


/* The card that holds a form. Restored after a bad edit deleted it: removing a
   shared selector list by cutting to the second close-brace-on-its-own-line skipped
   the indented inner braces of a media query and swallowed the next top-level rule
   with it. (No literal brace appears in this comment on purpose: a brace inside CSS
   prose defeats every naive index-based slice of this file, including the one that
   caused this, and LaunchContract counts raw braces as a tripwire for exactly that.)
   The class
   survived in the @supports backdrop-filter group below, so it still looked styled
   while having lost its border, radius, padding, background and shadow -- and both
   contact panels flattened. test_every_container_class_has_a_box_rule now catches
   that shape of loss. */
.form-card {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: clamp(1.75rem, 4.5vw, 2.75rem);
  background: var(--surface);
  box-shadow: var(--shadow-card);
}

.form-row {
  display: grid;
  gap: 1.25rem;
}

@media (min-width: 640px) {
  .form-row {
    grid-template-columns: 1fr 1fr;
  }
}

.field {
  display: grid;
  gap: 0.5rem;
  margin-bottom: 1.25rem;
}

.field label {
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--text-dim);
}

.field input,
.field select,
.field textarea {
  width: 100%;
  padding: 0.85rem 1rem;
  border-radius: 0.8rem;
  border: 1px solid var(--border);
  background: var(--field-bg);
  color: var(--text);
  transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease);
}

.field textarea {
  min-height: 9.5rem;
  resize: vertical;
}

.field input::placeholder,
.field textarea::placeholder {
  color: var(--text-faint);
}

.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: none;
  border-color: rgba(var(--accent-rgb), 0.6);
  box-shadow: 0 0 0 3px rgba(var(--glow-rgb), 0.14);
}

.field select {
  appearance: none;
  background-image: var(--select-chevron);
  background-repeat: no-repeat;
  background-position: right 1rem center;
}

.form-note {
  margin-top: 1rem;
  font-family: var(--font-mono);
  font-size: 0.64rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
  line-height: 2;
}

.form-note code {
  text-transform: none;
  letter-spacing: 0.04em;
}

/* Validation feedback — inline, announced, never colour-only */
.field__error {
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--refuse);
}

.field.has-error input,
.field.has-error select,
.field.has-error textarea {
  border-color: rgba(var(--refuse-rgb), 0.55);
}

.field.has-error input:focus,
.field.has-error select:focus,
.field.has-error textarea:focus {
  border-color: rgba(var(--refuse-rgb), 0.75);
  box-shadow: 0 0 0 3px rgba(var(--refuse-rgb), 0.14);
}

.form-status {
  margin-top: 1rem;
  font-size: 0.9rem;
  line-height: 1.6;
  color: var(--text-dim);
}

.form-status--error {
  color: var(--refuse);
}

.form-status--ok {
  color: var(--build);
}

/* Honeypot: off-screen for people, still in the form for bots */
.field--hp {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.form-success {
  text-align: center;
  padding: 2.5rem 1rem;
}

.form-success svg {
  width: 44px;
  height: 44px;
  margin-inline: auto;
  color: var(--accent);
}

.form-success h3 {
  margin-top: 1.1rem;
  font-size: 1.3rem;
}

.form-success p {
  margin-top: 0.7rem;
  color: var(--text-dim);
  font-size: 0.95rem;
  max-width: 26rem;
  margin-inline: auto;
}

.contact-alt {
  margin-top: 2.25rem;
  padding-top: 1.75rem;
  border-top: 1px solid var(--border);
  font-size: 0.95rem;
  color: var(--text-dim);
}

/* ------------------------------------------------------------------------
   THE THREE-STEP CONTACT FLOW

   Built out of the tokens and the existing .principle / .form-card idiom rather
   than a new visual language: same border, radius, surface gradient, hover lift
   and accent wash the rest of the site already uses, so a whole new interaction
   still reads as the same product. No literal colour appears in here, which is
   what lets the light theme inherit the block unchanged.
   ------------------------------------------------------------------------ */
.signal {
  position: relative;
}

/* ---- the progress rail ----------------------------------------------------
   Decorative: the panels carry the real semantics (role=group, a labelled
   heading, and `hidden` on the inactive steps), so the rail is aria-hidden at the
   digit level and never the thing a screen reader relies on to know where it is. */
.signal__progress {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem 1.4rem;
  margin: 0 0 2.4rem;
  padding: 0;
  list-style: none;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.signal__progress li {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  transition: color 0.3s var(--ease);
}

.signal__progress-n {
  display: grid;
  place-items: center;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  font-size: 0.6rem;
  transition: border-color 0.3s var(--ease), background-color 0.3s var(--ease), color 0.3s var(--ease);
}

.signal__progress li.is-current {
  color: var(--text);
}

.signal__progress li.is-current .signal__progress-n {
  border-color: rgba(var(--accent-rgb), 0.7);
  background: var(--accent-soft);
  color: var(--accent);
}

.signal__progress li.is-done {
  color: var(--text-dim);
}

.signal__progress li.is-done .signal__progress-n {
  border-color: rgba(var(--build-rgb), 0.5);
  color: var(--build);
}

/* ---- panels ---------------------------------------------------------------
   `hidden` does the hiding; the global `[hidden] { display: none !important }`
   rule already keeps these out of the tab order and the accessibility tree. The
   animation below therefore only ever runs on the panel becoming current. */
.signal__panel.is-current {
  animation: signal-enter 0.42s var(--ease) both;
}

@keyframes signal-enter {
  from {
    opacity: 0;
    transform: translateY(14px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

.signal__card {
  max-width: 44rem;
  margin-inline: auto;
}

.signal__chosen {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin: 0 0 1.4rem;
  font-size: 0.9rem;
  color: var(--text-dim);
}

.signal__chosen strong {
  color: var(--text);
  font-weight: 600;
}

/* An inline text button, not a link: it performs an action inside the flow rather
   than navigating, and a link that goes nowhere is worse than a button that does
   something. Sized and coloured to read as an affordance, not as body copy. */
.signal__edit {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
  padding: 0.1rem 0.35rem;
  border: 0;
  border-radius: 0.3rem;
  background: transparent;
  font: inherit;
  font-family: var(--font-mono);
  font-size: 0.64rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
  cursor: pointer;
  border-bottom: 1px solid rgba(var(--accent-rgb), 0.35);
  transition: color 0.2s var(--ease), border-color 0.2s var(--ease);
}

.signal__edit:hover,
.signal__edit:focus-visible {
  color: var(--text-bright);
  border-color: rgba(var(--accent-rgb), 0.8);
}

.signal__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.8rem;
  margin-top: 1.4rem;
}

.signal__actions .btn--primary {
  flex: 1 1 12rem;
}

.signal__alt {
  margin-top: 2rem;
  text-align: center;
}

/* ---- the category cards ----------------------------------------------------
   Each is a real <button>, so the whole card is the control: clicking anywhere on
   it selects, it is reachable by keyboard with no extra markup, and it has an
   accessible name from its own text. That is why the separate per-card email link
   is gone -- a link inside a button is invalid HTML and, worse, a small target
   inside a large one that people miss. */
.contact-categories {
  display: grid;
  gap: 1.1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
}

.contact-category {
  /* Undo the UA button box before anything else, or the card arrives with a
     centred label, a system font and a grey fill. */
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;

  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.7rem;
  padding: 1.75rem 1.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: linear-gradient(160deg, var(--glass), rgba(var(--surface-rgb), 0.34));
  transition: transform 0.35s var(--ease), border-color 0.35s var(--ease), box-shadow 0.35s var(--ease);
  overflow: hidden;
}

.contact-category::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(22rem 12rem at 85% -10%, rgba(var(--accent-rgb), 0.09), transparent 60%);
  opacity: 0;
  transition: opacity 0.4s var(--ease);
  pointer-events: none;
}

.contact-category:hover,
.contact-category:focus-visible {
  transform: translateY(-5px);
  border-color: rgba(var(--accent-rgb), 0.35);
  box-shadow: 0 30px 60px -34px rgba(var(--glow-rgb), 0.35);
}

.contact-category:hover::before,
.contact-category:focus-visible::before {
  opacity: 1;
}

/* :focus-within was the right selector when the card contained a link. With the
   card itself being the control it is :focus-visible, and leaving the old selector
   in place would style the card on any descendant focus that no longer exists. */
.contact-category__icon {
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border-radius: 12px;
  border: 1px solid rgba(var(--accent-rgb), 0.28);
  background: var(--accent-soft);
  color: var(--accent);
}

.contact-category__icon svg {
  width: 22px;
  height: 22px;
}

.contact-category__title {
  font-size: 1.12rem;
  font-weight: 600;
  color: var(--text);
}

/* flex: 1 pushes the call to action to the bottom of the card, so the five align
   on a row no matter how uneven the copy lengths are. */
.contact-category__body {
  flex: 1;
  margin: 0;
  color: var(--text-dim);
  font-size: 0.93rem;
  line-height: 1.65;
}

.contact-category__go {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.35rem;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
}

.contact-category__arrow {
  transition: transform 0.3s var(--ease);
}

.contact-category:hover .contact-category__arrow,
.contact-category:focus-visible .contact-category__arrow {
  transform: translateX(4px);
}

/* ---- the locked topic field ------------------------------------------------
   Readonly, not disabled: a disabled control is dropped from FormData, so the
   locked topic would vanish from the submission. The styling has to say "fixed"
   without saying "broken" -- a greyed-out field reads as an error, so this keeps
   the normal text colour and only changes the surface and the caret. */
.field input.is-locked {
  background: rgba(var(--overlay-rgb), 0.045);
  border-style: dashed;
  border-color: var(--border-strong);
  color: var(--text-dim);
  caret-color: transparent;
  cursor: default;
}

.field input.is-locked:focus {
  border-color: rgba(var(--accent-rgb), 0.45);
  box-shadow: none;
}

.field__hint {
  margin: 0.45rem 0 0;
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--text-faint);
}

@media (max-width: 560px) {
  .signal__actions {
    flex-direction: column;
    align-items: stretch;
  }

  .signal__actions .btn {
    width: 100%;
  }

  /* In a row, the desktop `flex: 1 1 12rem` is a width. In this column it is a
     HEIGHT, and the primary button grew into a 192px-tall oval between the
     fields and the next control. Let the button be its own height again. */
  .signal__actions .btn--primary {
    flex: none;
  }

  /* The step heading carries tabindex="-1" only so focus can move to it and a
     screen reader can announce the new panel. It is not a control, so the
     global accent focus ring reads as a stray box around the question after a
     tap. The panel change is the visible feedback; Tab still lands on the first
     field with its own ring. */
  .signal__panel [tabindex="-1"]:focus {
    outline: none;
  }

  .signal__progress {
    gap: 0.4rem 0.9rem;
    font-size: 0.6rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .signal__panel.is-current {
    animation: none;
  }

  .contact-category,
  .contact-category::before,
  .contact-category__arrow {
    transition: none;
  }

  .contact-category:hover,
  .contact-category:focus-visible {
    transform: none;
  }
}

/* ---------- 18. Credits rotary screen ---------- */
.credits-rotary-page {
  position: relative;
  height: 100vh;
  min-height: 100vh;
  overflow: hidden;
  overscroll-behavior: none;
  isolation: isolate;
  background: var(--bg);
}

/* The credits wheel composition, lifted above the global mesh.

   This page deliberately carries NO background layer of its own. It used to paint
   a second animated grid here, on top of the site-wide one, so the wheels sat over
   two lattices crossing each other; the mesh in section 2 is now the only thing
   behind them and it is the same mesh every other page has. What this rule owns is
   the stacking order -- z-index 1 against the canvas's 0 -- and isolation, so the
   wheels' own blurs and blend modes composite against the page rather than
   reaching down into the canvas. touch-action: none because the screen is entirely
   gesture driven and a browser scroll gesture would fight the wheel. */
.credits-rotary-page main.credits-rotary {
  position: relative;
  z-index: 1;
  width: 100%;
  height: 100vh;
  min-height: 100vh;
  overflow: hidden;
  isolation: isolate;
  touch-action: none;
}

/* Identical glass to .site-header, not a lookalike.

   This header used to paint its own gradient, its own 14px blur and a 0.055
   overlay border, so navigating between credits and any other page changed the
   bar's opacity, its edge contrast and the width of the line under it. It now
   consumes the same --header-glass-* tokens and the same declarations as the
   site header: same flat glass fill, same 20px saturating blur, same 1px edge,
   same shadow stack, same specular hairline. Only the internal grid differs,
   because this page centres a title instead of a nav. */
.credits-screen-header {
  position: fixed;
  inset: 0 0 auto;
  z-index: 60;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  width: 100%;
  height: var(--header-h);
  padding: 0 clamp(1rem, 3vw, 2.5rem);
  background: rgba(var(--header-glass-rgb), var(--header-glass-alpha));
  -webkit-backdrop-filter: var(--glass-filter);
  backdrop-filter: var(--glass-filter);
  border-bottom: 1px solid rgba(var(--header-glass-edge-rgb), 0.14);
  box-shadow:
    inset 0 1px 0 rgba(var(--header-glass-sheen-rgb), 0.18),
    inset 0 -1px 0 rgba(var(--header-glass-edge-rgb), 0.05),
    0 18px 44px -34px rgba(var(--header-glass-shadow-rgb), 0.65);
  transition: border-color 0.3s var(--ease), background-color 0.3s var(--ease),
    box-shadow 0.3s var(--ease);
  pointer-events: none;
}

.credits-screen-header::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent,
    rgba(var(--header-glass-sheen-rgb), 0.38) 28%,
    rgba(var(--header-glass-sheen-rgb), 0.2) 72%,
    transparent
  );
  pointer-events: none;
}

.credits-screen-brand,
.credits-screen-actions,
.credits-screen-exit {
  pointer-events: auto;
}

.credits-screen-brand {
  justify-self: start;
}

.credits-screen-actions {
  justify-self: end;
  display: inline-flex;
  align-items: center;
  gap: 1rem;
}

/* The large mono title balances the two oversized wheels. */
.credits-screen-header h1 {
  color: var(--text);
  font-family: var(--font-mono);
  font-size: clamp(1.5rem, 3.8vw, 2.7rem);
  font-weight: 500;
  letter-spacing: 0.42em;
  line-height: 1;
  text-indent: 0.42em;
}

/* The one link in the credits header, sized to the site's own .nav__link rather
   than to a credits-only scale: same 0.78rem, same 0.16em, same uppercase, same
   --text-dim resting colour and --text hover. It used to carry the .mono class as
   well, which set 0.72rem/0.16em and then had both of those overridden here --
   two rules fighting over one property is how a page drifts out of step with the
   site without anyone changing either file. It now sets its own type once. */
.credits-screen-exit {
  justify-self: end;
  display: inline-flex;
  align-items: center;
  min-height: 2.5rem;
  padding: 0.4rem 0.25rem;
  color: var(--text-dim);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  line-height: 1;
  transition: color 0.2s var(--ease);
}

.credits-screen-exit:hover,
.credits-screen-exit:focus-visible {
  color: var(--text);
}

.rotary-disc {
  /* 140vmin gives each corner wheel a substantial visible quadrant while
     preserving a true circle across wide and tall screens. */
  --rotary-disc-size: min(140vmin, 1400px);
  --sector-offset: 40deg;
  position: absolute;
  z-index: 2;
  width: var(--rotary-disc-size);
  height: var(--rotary-disc-size);
  pointer-events: none;
}

.rotary-disc--top {
  top: 0;
  left: 0;
  /* 44% sits the hub 6% of the disc INSIDE the corner rather than pinned to it.
     This one number is the sector-utilisation dial. At the old corner-pinned
     -50% only 19.7% of each wheel was on screen and the middle of the viewport
     was dead space; at -44% it is 25.8% on desktop and 26.0% on a phone (up
     from 7.3%), and the copy is dragged toward the centre with it.

     -44% is also the limit: it is the furthest either hub can travel before the
     two rims intersect on the tightest viewport measured (1024x768, which keeps
     +24px of clearance). Both breakpoints deliberately share this value, so the
     composition reads the same on a phone and a desktop and there is one number
     to reason about instead of two that can drift apart. */
  transform: translate(-44%, -44%);
  --sector-offset: 40deg;
}

.rotary-disc--bottom {
  right: 0;
  bottom: 0;
  /* Mirror of the upper hub, inset by the same 6% of the disc. */
  transform: translate(44%, 44%);
  --sector-offset: 220deg;
}

.rotary-disc__plate {
  position: absolute;
  inset: 0;
  border: 1px solid rgba(var(--violet-soft-rgb), 0.42);
  border-radius: 50%;
  background:
    repeating-conic-gradient(
      from calc(var(--sector-offset) - 60deg),
      rgba(var(--lavender-rgb), 0.22) 0deg 1deg,
      transparent 1deg 120deg
    ),
    radial-gradient(circle at 50% 50%, rgba(var(--accent-rgb), 0.1), transparent 42%),
    radial-gradient(circle at 50% 50%, rgba(var(--surface-rgb), 0.86) 0 63%, rgba(var(--bg-rgb), 0.88) 64% 100%);
  box-shadow:
    0 0 0 1px rgba(var(--overlay-rgb), 0.035),
    0 0 90px rgba(var(--glow-rgb), 0.14),
    inset 0 0 70px rgba(var(--glow-rgb), 0.11);
  transform: rotate(var(--disc-rotation, 0deg));
  transform-origin: 50% 50%;
  isolation: isolate;
  pointer-events: none;
  will-change: transform;
}

/* ------------------------------------------------------------------------
   WHEEL DECORATION -- no longer audio-reactive

   These layers used to read --wheel-bass, --wheel-onset and --wheel-treble,
   which initWheelReactivity() wrote onto each .rotary-disc every frame the music
   played. That function, its custom properties and every calc() below that
   consumed them are gone: the credits screen was running a per-frame analysis
   loop plus a dozen style recalculations on top of two large rotating discs with
   masks, conic gradients and drop-shadows, and that is what cost it frames.

   Every value below is now the RESTING value its calc() evaluated to with the
   music off, so this is a pure removal -- nothing gets brighter, dimmer or moves.
   The three scale() transforms are deleted rather than left at scale(1): each
   rule is position:absolute with its own z-index, so it already forms a stacking
   context and the transform was not carrying one.

   One thing flagged rather than silently "fixed": a few of these resting values
   were originally lowered to leave headroom for a flare that no longer exists --
   the dashed ring's 0.6 opacity and the spokes' 0.4 were 1.0 and 0.48 before the
   reactivity was added. They are left exactly as authored because restoring them
   is a visual decision, not part of removing the hook.
   ------------------------------------------------------------------------ */

/* A repeating conic ring supplies the cog silhouette without an image asset. */
.rotary-disc__plate::before {
  content: "";
  position: absolute;
  inset: -19px;
  z-index: 0;
  border-radius: 50%;
  background: repeating-conic-gradient(
    from 1deg,
    rgba(var(--accent-rgb), 0.78) 0deg 2.5deg,
    rgba(var(--surface-rgb), 0.2) 2.5deg 10deg
  );
  -webkit-mask: radial-gradient(transparent 0 68%, var(--mask-solid) 68.5% 76%, transparent 76.5%);
  mask: radial-gradient(transparent 0 68%, var(--mask-solid) 68.5% 76%, transparent 76.5%);
  opacity: 0.78;
  filter: drop-shadow(0 0 12px rgba(var(--glow-rgb), 0.32));
}

.rotary-disc__plate::after {
  content: "";
  position: absolute;
  inset: 8%;
  border: 1px dashed rgba(var(--violet-soft-rgb), 0.28);
  border-radius: 50%;
  box-shadow:
    0 0 0 1px rgba(var(--glow-blue-rgb), 0.08),
    0 0 16px rgba(var(--glow-rgb), 0.08);
  opacity: 0.6;
  z-index: 1;
  pointer-events: none;
}

.rotary-disc__ring {
  position: absolute;
  display: block;
  border: 1px dashed rgba(var(--muted-rgb), 0.2);
  border-radius: 50%;
  /* Empty <span>s, concentric with the disc. */
  opacity: 0.72;
  z-index: 2;
  pointer-events: none;
}

.rotary-disc__ring--one {
  inset: 18%;
  border-color: rgba(var(--blue-rgb), 0.23);
}

.rotary-disc__ring--two {
  inset: 33%;
  border-style: dotted;
  border-color: rgba(var(--accent-rgb), 0.42);
}

.rotary-disc__ring--three {
  inset: 47%;
  border-color: rgba(var(--overlay-rgb), 0.11);
}

/* The track is intentionally divided into three and only ever advances in
   120-degree increments once the magnetic settle has finished. */
.rotary-disc__sector-track {
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: 50%;
  pointer-events: none;
}

.rotary-disc__sector-track::before {
  content: "";
  position: absolute;
  inset: 0;
  /* Below the detail blocks (z-index 4). At 5 these decorative radial lines
     painted straight across the contributor's copy. */
  z-index: 1;
  border-radius: 50%;
  background: repeating-conic-gradient(
    from calc(var(--sector-offset) - 60deg),
    rgba(var(--lavender-rgb), 0.7) 0deg 1deg,
    transparent 1deg 120deg
  );
  -webkit-mask: radial-gradient(transparent 0 7%, var(--mask-solid) 7.2% 100%);
  mask: radial-gradient(transparent 0 7%, var(--mask-solid) 7.2% 100%);
  /* Opacity only, and never a transform: this pseudo-element belongs to the
     sector track, which is the subtree the contributor copy lives in. */
  opacity: 0.4;
  pointer-events: none;
}

/* Both wheels now share ONE segment model: no clip-path, no overflow hiding.

   The upper wheel used to clip its sectors to a 120-degree wedge. That silently
   cut the contribution copy on every narrow viewport -- its box corners reach
   64-78 degrees against a 60-degree limit -- and it would have cut the
   rim-mounted role sign as well. The wedge is purely decorative and is already
   drawn twice over, by the plate's repeating-conic-gradient and the sector
   track's radial lines, so nothing visual is lost by dropping the clip. The
   three detail blocks cannot collide with each other either: they are always
   exactly 120 degrees apart around the hub. */
.rotary-segment {
  position: absolute;
  inset: 0;
  transform: rotate(var(--segment-angle));
  transform-origin: 50% 50%;
  transition: opacity 0.2s var(--ease);
}

/* Positive top-wheel rotation carries member 1, then member 2, into the
   active 40-degree sector. The lower wheel rotates in the opposite direction. */
.rotary-disc--top .rotary-segment[data-rotary-segment="0"] {
  --segment-angle: 40deg;
}

.rotary-disc--top .rotary-segment[data-rotary-segment="1"] {
  --segment-angle: 280deg;
}

.rotary-disc--top .rotary-segment[data-rotary-segment="2"] {
  --segment-angle: 160deg;
}

.rotary-disc--bottom .rotary-segment[data-rotary-segment="0"] {
  --segment-angle: 220deg;
}

.rotary-disc--bottom .rotary-segment[data-rotary-segment="1"] {
  --segment-angle: 340deg;
}

.rotary-disc--bottom .rotary-segment[data-rotary-segment="2"] {
  --segment-angle: 100deg;
}

.rotary-segment__content {
  --segment-scale: 0.6;
  --segment-opacity: 0.3;
  position: absolute;
  /* Each block is centred on this point rather than hung from its top-left
     corner, so where it lands does not depend on how tall its own copy happens
     to be -- a longer bio used to push the whole block off its mark, through the
     viewport edge and into the status bar.

     The two blocks no longer share ONE arm. They used to, and that is a real
     constraint rather than a tidy-up: the sectors are exactly 180 degrees apart
     and the hubs sit on opposite corners, so top = hub + u forces
     bottom = hub' - u. Both target zones are then reachable only if they are
     symmetric about the viewport centre, and the requested ones are not (they
     differ by ~48px horizontally and ~36px vertically at 1440x900). Sharing an
     arm split that error across both blocks and left the profile block ~74px
     short of its zone. Giving --contribution and --profile their own top/left
     breaks the coupling.

     Solved by constrained search over 14 viewports against the four hard
     contracts the suite already asserts: the safe area (24px from every edge,
     clear of the header and the status bar), the zone each wheel owns, no
     overlap between the two blocks, and no collision with the rim label sharing
     the block's ray -- on all three landing rays, with the two non-settled
     blocks measured at the 0.6 scale they really render at. The objective is the
     distance to the boxes the design review asked for: worst case 122px desktop,
     96px on a phone. "Fully inside the wheel rim" is deliberately NOT one of the
     constraints because it is provably impossible on a phone -- a 230px block
     inside a 421px disc leaves 57px of reach, while the header and padding need
     184px. See "Credits rotary geometry" in the README. */
  top: 58%;
  left: 76%;
  z-index: 4;
  opacity: var(--segment-opacity);
  transform: translate(-50%, -50%) rotate(calc(0deg - var(--segment-angle) - var(--disc-rotation, 0deg))) scale(var(--segment-scale));
  transform-origin: center;
  transition: opacity 0.18s var(--ease), filter 0.18s var(--ease);
  will-change: transform, opacity;
}

.rotary-segment.is-active .rotary-segment__content {
  --segment-scale: 1;
  --segment-opacity: 1;
}

/* The mesh is now bright enough to compete with the copy, and the copy has no
   background of its own -- it sits directly on the canvas. So each block carries
   a soft scrim: a radial fade of the page background painted behind the text but
   above the canvas (z-index -1 inside the block's own stacking context). It has
   no hard edge, and because it is a child of the block it inherits
   --segment-opacity, so the two non-settled blocks fade their scrims out along
   with their copy instead of leaving dark blobs on the wheel. */
.rotary-segment__content::before {
  content: '';
  position: absolute;
  inset: -24% -18%;
  z-index: -1;
  border-radius: 50%;
  /* Back to the 0.9/0.6 this shipped with. It had been thickened to 0.93/0.74 for
     one round, when the mesh was at 0.45-0.60 alpha and the paragraph measured
     2.39:1 with no scrim at all -- the scrim was the only thing holding the credits
     copy up. With the mesh back down to 0.16-0.22 the copy clears AA on the bare
     mesh (5.59:1 dark, 5.89:1 light), so the scrim is insurance again rather than
     structure, and the lighter gradient leaves less of a visible blob behind the
     text. Kept at all because it is what stops the NEXT brightness increase from
     silently becoming a legibility regression. */
  background: radial-gradient(closest-side,
    rgba(var(--bg-rgb), 0.9),
    rgba(var(--bg-rgb), 0.6) 52%,
    rgba(var(--bg-rgb), 0) 78%);
  pointer-events: none;
}

/* The arriving contributor.

   Each segment permanently owns its member, so no copy is ever rewritten when
   the wheel settles -- the only thing that changes is WHICH block is emphasised.
   The previous version dimmed every block to nothing, waited, "swapped" text
   that was already correct, and faded back: a full-screen blink on every sector
   change, fighting the emphasis the spring was already ramping per frame. This
   animates the arrival instead, so the incoming block eases up into place while
   the outgoing one is easing down under the same spring.

   The animation is applied to the CHILDREN, never to .rotary-segment__content or
   .rotary-sign-hanger themselves. Those two are counter-rotated every animation
   frame; a transform animation on either would fight that and smear the wheel
   into a visible lag. Their children are not frame-driven, so they are safe.

   0.42s must stay equal to ARRIVAL_MS in app.js, which clears the marker when
   the ease-up finishes. A test asserts the two agree. */
@keyframes rotary-arrive {
  from {
    opacity: 0;
    transform: translateY(12px);
    filter: blur(3px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
    filter: blur(0);
  }
}

.rotary-segment__content.is-arriving > * {
  animation: rotary-arrive 0.42s var(--ease) both;
}

@media (prefers-reduced-motion: reduce) {
  .rotary-segment__content.is-arriving > * {
    animation: none;
  }
}

.rotary-segment__content--contribution {
  /* Narrower and taller than before, so the paragraph wraps clear of the wheel's
     inner ring instead of running across it. The 230px floor keeps it readable
     on a phone: the review's bare min(23vw, 340px) is 90px at 390px wide, about
     eleven characters a line. */
  width: min(max(23vw, 230px), 340px);
  min-height: 24vh;
}

.rotary-disc__contributions {
  max-width: 30rem;
  color: var(--text-dim);
  font-size: clamp(0.95rem, calc(var(--rotary-disc-size) * 0.018), 1.35rem);
  line-height: 1.65;
}


/* Each role sign is a ferris-wheel passenger hanging from its OWN sector's rim.

   The hanger now lives inside its segment, so it inherits that segment's
   --segment-angle and orbits with it -- there are no per-index coordinates left
   to get wrong. Previously three hand-authored top/left pairs placed them, and
   two of the three were a full 120 degrees away from the sector they belonged
   to, which is why "Design & ideas" and "The public" read as intermixed while
   "Developer" looked correct.

   The plate supplies the wheel rotation, the segment supplies its base angle,
   and the hanger subtracts both before adding app.js's small spring swing, so
   the sign stays upright and swings from the cord. */
.rotary-sign-hanger {
  position: absolute;
  top: 50%;
  /* 99.7% puts the cord's pivot at 49.7% of the disc out from the hub -- the
     radius "Developer" already hung at, which was the one that looked right. */
  left: 99.7%;
  z-index: 7;
  position: relative;
  width: max-content;
  height: clamp(2.5rem, calc(var(--rotary-disc-size) * 0.06), 5rem);
  opacity: 0.82;
  transform: translate(-50%, 0) rotate(calc(0deg - var(--segment-angle, 0deg) - var(--disc-rotation, 0deg) + var(--tag-swing-angle, 0deg)));
  transform-origin: 50% 0;
  transition: opacity 0.22s var(--ease);
  will-change: transform;
}

.rotary-sign-hanger.is-active {
  opacity: 1;
}

.rotary-sign-hanger__cord {
  position: absolute;
  top: 0;
  left: 50%;
  width: 1px;
  height: clamp(2.5rem, calc(var(--rotary-disc-size) * 0.06), 5rem);
  transform-origin: 0 0;
  background: linear-gradient(180deg, rgba(var(--lavender-rgb), 0.8), rgba(var(--blue-rgb), 0.28));
  box-shadow: 0 0 12px rgba(var(--glow-rgb), 0.42);
}

.rotary-sign-hanger__cord--left {
  transform: rotate(-55deg);
}

.rotary-sign-hanger__cord--right {
  transform: rotate(55deg);
}

.rotary-sign,
.rotary-name-tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  height: 2.5rem;
  min-width: clamp(8rem, calc(var(--rotary-disc-size) * 0.14), 11rem);
  padding: 8px 16px;
  /* 0.88 is a measured value, not a stylistic one: it is the resting rim alpha a
     contrast check derives its floor from, for a hairline drawn ON a saturated
     brand-gradient fill. */
  border: 1px solid rgba(var(--rim-rgb), 0.88);
  border-radius: 0.25rem;
  color: var(--on-gradient);
  background: var(--brand-gradient-ink);
  box-shadow:
    0 10px 28px -14px rgba(var(--glow-blue-rgb), 0.88),
    0 0 24px rgba(var(--glow-rgb), 0.34);
  font-family: var(--font-mono);
  font-size: clamp(0.67rem, calc(var(--rotary-disc-size) * 0.011), 0.95rem);
  font-weight: 600;
  letter-spacing: 0.16em;
  line-height: 1;
  text-align: center;
  text-transform: uppercase;
  white-space: nowrap;
}

.rotary-sign {
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%) scale(var(--tag-scale, 0.76));
  transform-origin: 50% 0;
  transition: transform 0.22s var(--ease);
}

.rotary-segment__content--profile {
  display: grid;
  justify-items: center;
  gap: clamp(0.9rem, calc(var(--rotary-disc-size) * 0.015), 1.5rem);
  /* No padding is reserved for the name plate: it hangs from the rim outside
     this card again, so the card is exactly the portrait plus its bio. */
  /* Its own arm: see .rotary-segment__content. Width floored at 200px so the
     110px avatar and its caption always fit -- the review's bare min(17.5vw,
     260px) is 68px on a 390px phone, narrower than the avatar it contains. */
  top: 64%;
  left: 76.5%;
  width: min(max(17.5vw, 200px), 260px);
  text-align: center;
}

.rotary-profile__photo-frame {
  position: relative;
  display: grid;
  place-items: center;
  width: clamp(110px, calc(var(--rotary-disc-size) * 0.14), 210px);
  height: clamp(110px, calc(var(--rotary-disc-size) * 0.14), 210px);
  overflow: hidden;
  border: 4px solid rgba(var(--lavender-rgb), 0.86);
  border-radius: 50%;
  background:
    radial-gradient(circle at 35% 28%, rgba(var(--lavender-rgb), 0.2), transparent 42%),
    linear-gradient(145deg, rgba(var(--accent-deep-rgb), 0.26), rgba(var(--bg-rgb), 0.94));
  box-shadow: 0 0 0 5px rgba(var(--glow-rgb), 0.14), 0 0 35px rgba(var(--glow-blue-rgb), 0.24);
}

.rotary-profile__photo {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  object-fit: cover;
  opacity: 0;
}

.rotary-profile__photo.is-mark {
  padding: 1.2rem;
  object-fit: contain;
}

.rotary-profile__photo-frame.is-ready .rotary-profile__photo {
  opacity: 1;
}

.rotary-profile__pending {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  gap: 0.35rem;
  padding: 1rem;
  color: rgba(var(--text-rgb), 0.88);
  text-align: center;
}

.rotary-profile__pending strong {
  color: var(--lavender);
  font-size: clamp(1.5rem, calc(var(--rotary-disc-size) * 0.035), 2.6rem);
  letter-spacing: 0.08em;
}

.rotary-profile__pending small {
  color: rgba(var(--text-rgb), 0.56);
  font-family: var(--font-mono);
  font-size: clamp(0.48rem, calc(var(--rotary-disc-size) * 0.008), 0.68rem);
  letter-spacing: 0.12em;
  line-height: 1.35;
  text-transform: uppercase;
}

.rotary-profile__photo-frame.is-ready .rotary-profile__pending {
  display: none;
}

.rotary-segment__bio {
  color: var(--text-dim);
  font-size: clamp(0.82rem, calc(var(--rotary-disc-size) * 0.012), 1.05rem);
  line-height: 1.55;
}

/* Each profile segment owns a static name plate at its local outer rim.
   The article rotates with the sector; the formula below subtracts that
   sector's base angle and adds the JS-supplied inverse wheel rotation, so the
   plate orbits the circumference and stays upright as the wheel turns.

   The plate deliberately lives OUTSIDE .rotary-segment__content--profile: the
   card scales to 0.6 whenever it is not the settled sector, and a plate inside
   it would scale, rotate and travel with the copy instead of hanging from the
   rim the visitor is reading. Only emphasis is JS-driven (.is-active). */
.rotary-name-tag {
  position: absolute;
  top: 50%;
  left: 96%;
  z-index: 8;
  opacity: 0.82;
  transform: translate(-50%, -50%) rotate(calc(0deg - var(--segment-angle, 0deg) + var(--tag-counter-rotation, 0deg)));
  transform-origin: 50% 50%;
  transition: opacity 0.22s var(--ease);
  will-change: transform;
}

.rotary-name-tag.is-active {
  opacity: 1;
}

/* A short hairline ties the plate back to the rim it hangs from. */
.rotary-name-tag::after {
  content: "";
  position: absolute;
  right: -2.2rem;
  top: 50%;
  width: 2.2rem;
  height: 1px;
  background: linear-gradient(90deg, var(--blue), transparent);
}

.rotary-name-tag__line {
  position: absolute;
  left: 0.55rem;
  top: 0.55rem;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--blue);
  box-shadow: 0 0 10px var(--blue);
}

.credits-rotary-status {
  position: fixed;
  left: 50%;
  bottom: max(1rem, env(safe-area-inset-bottom));
  z-index: 55;
  display: flex;
  align-items: center;
  gap: 1.1rem;
  min-height: 2.8rem;
  /* A shrink-to-fit fixed element sizes itself against the space from its
     left:50% anchor to the right edge, so at 1024px the long colophon squeezed
     the hint and the counter to 8px wide and wrapped them one letter per line:
     a 258px-tall bar straight through the profile card. The bar now keeps its
     natural width up to the viewport gutter, and its three children never
     break. */
  max-width: calc(100vw - 2rem);
  padding: 0.7rem 1rem;
  border: 1px solid rgba(var(--overlay-rgb), 0.08);
  border-radius: 9999px;
  color: var(--text-faint);
  background: rgba(var(--bg-rgb), 0.66);
  -webkit-backdrop-filter: var(--glass-filter);
  backdrop-filter: var(--glass-filter);
  transform: translateX(-50%);
  pointer-events: none;
}

.credits-rotary-status__hint {
  color: var(--violet-soft);
  font-size: clamp(0.68rem, 1vw, 0.78rem);
  letter-spacing: 0.15em;
  text-transform: uppercase;
  white-space: nowrap;
}

.credits-rotary-status__count {
  color: var(--text-dim);
  font-size: clamp(0.66rem, 0.95vw, 0.76rem);
  letter-spacing: 0.12em;
  white-space: nowrap;
}

/* The bottom-bar colophon: same slot the contact link occupied, so it inherits
   that link's sizing and only changes what a static line needs -- no hover, no
   pointer-events override, nothing to click. */
.credits-screen-colophon {
  white-space: nowrap;
}

.credits-scroll-affordance {
  position: fixed;
  left: 50%;
  bottom: max(4.8rem, calc(env(safe-area-inset-bottom) + 3.8rem));
  z-index: 54;
  display: grid;
  justify-items: center;
  gap: 0.45rem;
  color: var(--text-faint);
  opacity: 1;
  pointer-events: none;
  transform: translateX(-50%);
  transition: opacity 0.45s var(--ease), visibility 0.45s var(--ease);
}

.credits-scroll-affordance.is-hidden {
  visibility: hidden;
  opacity: 0;
}

.credits-scroll-affordance__label {
  color: var(--lavender);
  font-size: clamp(0.66rem, 0.9vw, 0.78rem);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  white-space: nowrap;
}

.credits-scroll-affordance__mouse {
  position: relative;
  display: block;
  width: 1.3rem;
  height: 1.9rem;
  border: 1px solid rgba(var(--lavender-rgb), 0.82);
  border-radius: 999px;
  box-shadow: 0 0 18px rgba(var(--glow-rgb), 0.24);
}

.credits-scroll-affordance__wheel {
  position: absolute;
  top: 0.32rem;
  left: 50%;
  width: 3px;
  height: 0.38rem;
  border-radius: 999px;
  background: var(--violet-soft);
  transform: translateX(-50%);
  animation: credits-scroll-wheel 1.7s ease-in-out infinite;
}

.credits-scroll-affordance__chevron {
  display: block;
  width: 0.55rem;
  height: 0.55rem;
  margin-top: -0.15rem;
  border-right: 1px solid var(--violet-soft);
  border-bottom: 1px solid var(--violet-soft);
  transform: rotate(45deg);
  animation: credits-scroll-chevron 1.7s ease-in-out infinite;
}

@keyframes credits-scroll-wheel {
  0%, 100% { opacity: 0.45; transform: translate(-50%, 0); }
  50% { opacity: 1; transform: translate(-50%, 0.38rem); }
}

@keyframes credits-scroll-chevron {
  0%, 100% { opacity: 0.4; transform: translateY(0) rotate(45deg); }
  50% { opacity: 1; transform: translateY(0.24rem) rotate(45deg); }
}

@media (max-width: 768px) {
  /* Mobile safe zone: the wheels remain circular but reveal only shallow
     corner arcs, leaving a clear, usable band through the middle. */
  .rotary-disc {
    --rotary-disc-size: min(108vmin, 520px);
  }

  /* No hub override here on purpose. Narrow screens used to push each hub 15%
     of the disc OFF-screen (translate(-65%,-65%)), which threw away most of the
     wheel -- only 7.3% of it was visible -- and forced the detail blocks onto a
     137% arm to reach past it. Both breakpoints now share the base -44% inset,
     so the wheel is 3.6x more present on a phone and the arm can be shorter. */

  /* The height comes from --header-h, exactly as it does on every other page.
     A hardcoded 58px here fought the token at the 768px breakpoint (62px) and
     made the credits bar 4px shorter than the site header during a PJAX swap. */
  .credits-screen-header {
    grid-template-columns: 1fr auto;
  }

  .credits-screen-header h1 {
    position: absolute;
    left: 50%;
    top: 50%;
    max-width: calc(100vw - 8rem);
    font-size: clamp(0.88rem, 4.4vw, 1.2rem);
    letter-spacing: 0.18em;
    text-indent: 0.18em;
    transform: translate(-50%, -50%);
    white-space: nowrap;
  }

  /* Was `> a:first-child { display: none }` plus `.credits-screen-exit { font-size: 0 }`
     with the label hidden and only the ↗ span sized back up. That hid "Why Agora" and
     reduced the exit to a bare arrow. With one link left, the first rule would have
     hidden the only way off the credits screen on a phone, and the second would have
     left it invisible anyway -- the arrow span it re-sized is gone. The label is now
     shown, small, because it is the only navigation this screen has. */
  .credits-screen-exit {
    font-size: 0.62rem;
    letter-spacing: 0.14em;
    padding: 0.4rem 0.2rem;
  }

  /* Re-solved against the boxes that actually render, not the 168px model the
     old suite used. The paragraph is still 230px wide, and the role sign hangs
     from the upper-left rim, so the description has to sit below the sign and
     the profile card has to sit below the description on the 16:9 phones. The
     arm that satisfies every mobile viewport at once is 110/74 for the copy and
     108/78 for the card; no short viewport loses a block to the status bar. */
  .rotary-segment__content {
    top: 74%;
    left: 110%;
  }

  /* The 24vh floor made an invisible 200px-tall box around three lines of copy.
     That box -- not the text -- was what collided with the profile card on a
     phone. The copy itself is only ~70px tall, so the floor is removed here and
     the block is measured by what a visitor can see. */
  .rotary-segment__content--contribution {
    min-height: 0;
  }

  /* No width override here on purpose. The base rule is already viewport-relative
     with a readable floor, so re-declaring a rem/vw mix would only hand the
     geometry solver a second set of numbers to disagree with. */

  .rotary-disc__contributions {
    font-size: clamp(0.7rem, calc(var(--rotary-disc-size) * 0.014), 0.88rem);
    line-height: 1.45;
  }

  .rotary-segment__content--profile {
    top: 78%;
    left: 108%;
    gap: 0.25rem;
  }

  .rotary-profile__photo-frame {
    width: clamp(85px, calc(var(--rotary-disc-size) * 0.18), 120px);
    height: clamp(85px, calc(var(--rotary-disc-size) * 0.18), 120px);
    border-width: 3px;
  }

  .rotary-segment__bio {
    font-size: clamp(0.64rem, calc(var(--rotary-disc-size) * 0.01), 0.82rem);
    line-height: 1.4;
  }

  /* Keep both wheel tag families identical while making long names fit a
     narrow portrait viewport. */
  .rotary-sign,
  .rotary-name-tag {
    height: 2.15rem;
    min-width: min(7.2rem, calc(100vw - 1.5rem));
    max-width: calc(100vw - 1.5rem);
    padding: 8px 12px;
    overflow: hidden;
    font-size: clamp(0.48rem, 2.1vw, 0.58rem);
    letter-spacing: 0.08em;
    text-overflow: ellipsis;
  }

  /* The plate stays on the true rim (left: 96%) on phones too. Pulling it in
     to 84% detached it from the circumference the visitor can see. */
  .rotary-name-tag::after {
    right: -1rem;
    width: 1rem;
  }

  /* One rule for all three signs: 41.2% of the disc out from the hub, which is
     the radius the "Developer" sign already hung at here. */
  .rotary-sign-hanger {
    left: 91.2%;
  }

  /* A 40px cord on a phone dropped the sign 14px further down than it needed to
     go and ate the gap above the description. Scale it with the disc, with a
     24px floor so the cord still reads as a cord. */
  .rotary-sign-hanger__cord {
    height: clamp(1.5rem, calc(var(--rotary-disc-size) * 0.06), 5rem);
  }

  .rotary-sign-hanger {
    height: clamp(1.5rem, calc(var(--rotary-disc-size) * 0.06), 5rem);
  }

  /* A compact two-line strip pinned to the very bottom of a phone: the hint
     and counter share the first line, the colophon centres on the second.
     It used to sit 4.35rem up the screen (to clear the sound control), which
     is exactly where the rim-mounted name plate now hangs. */
  .credits-rotary-status {
    left: 50%;
    bottom: max(0.5rem, env(safe-area-inset-bottom));
    width: calc(100vw - 1.5rem);
    max-width: none;
    min-height: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: center;
    gap: 0.12rem 0.7rem;
    padding: 0.36rem 0.7rem 0.4rem;
    border-radius: 0.85rem;
    transform: translateX(-50%);
  }

  .credits-rotary-status__hint {
    font-size: 0.56rem;
    letter-spacing: 0.1em;
  }

  .credits-rotary-status__count,
  .credits-screen-colophon {
    font-size: 0.56rem;
  }

  /* Always its own line, so the first row stays a tidy hint/counter pair and
     the colophon cannot push either of them onto a third line. */
  .credits-screen-colophon {
    flex-basis: 100%;
    text-align: center;
    line-height: 1.45;
    letter-spacing: 0.08em;
  }

  /* The status bar owns the bottom edge now, so the sound control sits just
     above it on the left -- clear of the status, the wheel and the rim plate.
     Like every other phone page it is an icon-only circle at this width. */
  .credits-rotary-page .ambient-toggle {
    right: auto;
    left: 0.75rem;
    bottom: calc(max(0.5rem, env(safe-area-inset-bottom)) + 4.2rem);
    width: 2.15rem;
    min-height: 2.15rem;
    padding: 0;
    font-size: 0.56rem;
  }

  /* The rim name plate and the scroll cue want the same narrow band above the
     status bar. The cue is decorative (aria-hidden) and duplicates the "wheel
     to rotate" hint that is already in the status bar, so it yields. */
  .credits-scroll-affordance {
    display: none;
  }
}

/* Between 769 and 1000px the centred status bar reaches the sound control in
   the bottom-right corner. Raise the control clear of the bar on this screen. */
@media (min-width: 769px) and (max-width: 1000px) {
  .credits-rotary-page .ambient-toggle {
    bottom: calc(1rem + 3.5rem);
  }
}

@media (prefers-reduced-motion: reduce) {
  .rotary-disc__plate,
  .rotary-disc__sector-track,
  .rotary-segment,
  .rotary-segment__content,
  .rotary-sign-hanger,
  .rotary-name-tag,
  .credits-scroll-affordance,
  .credits-scroll-affordance__wheel,
  .credits-scroll-affordance__chevron {
    transition: none;
    animation: none;
  }
}

/* ---------- 19. Footer ---------- */
.site-footer {
  border-top: 1px solid var(--border);
  background: rgba(var(--bg-soft-rgb), 0.64);
  padding-top: clamp(2.75rem, 6vw, 4rem);
}

.site-footer__grid {
  display: grid;
  gap: 2.25rem;
  padding-bottom: 2.5rem;
}

@media (min-width: 768px) {
  .site-footer__grid {
    grid-template-columns: 1.4fr 1fr 1fr;
    gap: 3rem;
  }
}

.site-footer h4 {
  font-family: var(--font-mono);
  font-size: 0.64rem;
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 1.1rem;
}

/* The eight page links are one short block, not a column taller than the
   brand. Two columns keep the Pages group square and use the width that used
   to sit empty between it and Contact. */
.site-footer__links {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(1rem, 2.4vw, 2rem);
  align-content: start;
}

.site-footer__brand p {
  margin-top: 1rem;
  color: var(--text-dim);
  font-size: 0.92rem;
  max-width: 20rem;
  line-height: 1.7;
}

.site-footer__signal {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  margin-top: 1.4rem;
  font-family: var(--font-mono);
  font-size: 0.64rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent);
}

.site-footer__signal i {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 10px rgba(var(--glow-rgb), 0.9);
  animation: pulse 2.6s ease-in-out infinite;
}

/* Platform availability lives with the signal, not in the Contact column:
   it is a status line about the product, not a way to reach the team.

   Deliberately NOT uppercased: text-transform turned the platform name "iOS"
   into "IOS" on every footer. The mixed case is the correct spelling. */
.site-footer__platforms {
  margin-top: 0.85rem;
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.04em;
  line-height: 1.75;
}

@media (prefers-reduced-motion: reduce) {
  .site-footer__signal i {
    animation: none;
  }
}

.site-footer nav a {
  display: block;
  padding: 0.4rem 0;
  color: var(--text-dim);
  font-size: 0.92rem;
  transition: color 0.2s var(--ease), transform 0.25s var(--ease);
}

.site-footer nav a:hover {
  color: var(--accent);
  transform: translateX(4px);
}

.site-footer__contact p {
  color: var(--text-dim);
  font-size: 0.92rem;
  line-height: 2;
}

.site-footer__contact a {
  color: var(--text);
  border-bottom: 1px solid var(--border-strong);
  transition: color 0.2s var(--ease), border-color 0.2s var(--ease);
}

.site-footer__contact a:hover {
  color: var(--accent);
  border-color: var(--accent);
}

.site-footer__bottom {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 2rem;
  align-items: center;
  justify-content: space-between;
  padding: 1.4rem 0 1.6rem;
  border-top: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* Shared glass surfaces, with an opaque fallback for older browsers.

   Every translucent surface on the site now consumes ONE recipe,
   --glass-filter, so a card, the header, the mobile menu and the credits
   status bar all frost the page the same way. The group is split because
   panels swap their opaque fallback for the shared see-through --glass fill,
   while chrome that already paints its own translucent fill only borrows the
   filter. */
@supports ((backdrop-filter: blur(18px)) or (-webkit-backdrop-filter: blur(18px))) {
  .site-header,
  .credits-screen-header,
  .ambient-toggle,
  .credits-rotary-status,
  .mobile-menu,
  .section--alt,
  .site-footer,
  .privacy-copy,
  .principle,
  .cta-panel,
  .dl-card,
  .shared-card,
  .post-preview,
  .peek,
  .creator,
  .creator__card,
  .form-card,
  .standard,
  .step,
  .contact-callout,
  .contact-category,
  .secure-note {
    -webkit-backdrop-filter: var(--glass-filter);
    backdrop-filter: var(--glass-filter);
  }

  /* Panels use the shared see-through fill instead of opaque paint, so the
     blur has something to show through. */
  .principle,
  .cta-panel,
  .dl-card,
  .shared-card,
  .post-preview,
  .peek,
  .creator,
  .creator__card,
  .form-card,
  .standard,
  .step {
    background: var(--glass);
  }
}

/* ---------- 20. Reveal-on-scroll ---------- */
[data-reveal] {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
}

[data-reveal].is-visible {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  [data-reveal] {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ---------- 21. Responsive overrides (max-width) ----------
   The base styles above are mobile-first; these queries tighten
   spacing and stacking for tablets and phones where the fluid
   clamp() values alone are not enough.
   ------------------------------------------------------------ */

/* Tablet & below --------------------------------------------- */
@media (max-width: 768px) {
  :root {
    --header-h: 62px;
    --radius: 1.1rem;
    --radius-lg: 1.4rem;
  }

  .section {
    padding-block: clamp(3.25rem, 9vw, 5rem);
  }

  .section-head {
    margin-bottom: 2rem;
  }

  .display {
    font-size: clamp(2.35rem, 9.5vw, 3.6rem);
  }

  .h2 {
    font-size: clamp(1.65rem, 5.5vw, 2.3rem);
  }

  .hero {
    padding-top: calc(var(--header-h) + 2.5rem);
  }

  .hero__grid {
    gap: 2.75rem;
  }

  .hero__visual {
    margin-top: 0.5rem;
  }

  .hero__visual .phone {
    --phone-w: 270px;
  }

  /* Decorative orbs: keep them subtle and well inside the canvas */
  .orb--one {
    width: 22rem;
    height: 22rem;
    right: -11rem;
  }

  .orb--two {
    width: 15rem;
    height: 15rem;
    right: -7rem;
  }

  .quote .orb--one {
    left: -12rem;
  }

  .principle {
    padding: 1.6rem 1.4rem;
  }

  .cta-panel {
    padding: 2.1rem 1.5rem;
  }

  .cta-panel h2 {
    font-size: clamp(1.75rem, 6.5vw, 2.6rem);
  }

  .dl-card,
  .creator,
  .form-card,
  .shared-card {
    padding: 1.6rem 1.35rem;
  }

  .dl-card__actions .btn {
    width: 100%;
  }

  .spec-list li {
    font-size: 0.92rem;
    gap: 1rem;
  }

  .spec-list .k {
    flex: none;
  }

  .step,
  .standard {
    padding: 1.5rem 1.35rem;
  }

  /* Was a shared rule with .contact-grid .section-head, which is gone: the
     contact section moved to its own page and no longer uses that grid. Only the
     surviving selector is left -- editing a selector LIST by cutting from the
     second name onwards leaves the first one dangling into whatever rule follows,
     which silently re-targets it. */
  .vision-grid .section-head {
    position: static;
  }

  .site-footer__grid {
    gap: 2rem;
  }

  .site-footer__bottom {
    justify-content: flex-start;
    gap: 0.5rem 1.5rem;
  }

  .slim-footer .container {
    justify-content: center;
    text-align: center;
  }
}

/* Phone --------------------------------------------------------- */
@media (max-width: 480px) {
  :root {
    --header-h: 58px;
  }

  body {
    font-size: 0.97rem;
  }

  .container {
    width: min(1120px, 100% - 2rem);
  }

  .nav__logo {
    gap: 0.55rem;
  }

  .nav__logo-mark {
    width: 38px;
    height: 38px;
  }

  .nav__logo-name {
    font-size: 1.08rem;
  }

  .nav__logo--footer .nav__logo-name {
    font-size: 1.15rem;
  }

  .nav__toggle {
    width: 40px;
    height: 40px;
  }

  .mobile-menu {
    padding: 0.6rem 1rem 1.4rem;
  }

  .eyebrow {
    font-size: 0.62rem;
    letter-spacing: 0.22em;
    gap: 0.55rem;
  }

  .eyebrow::before,
  .eyebrow--center::after {
    width: 1.3rem;
  }

  .display {
    font-size: clamp(2.15rem, 11vw, 2.8rem);
    letter-spacing: -0.03em;
  }

  .h2 {
    font-size: clamp(1.5rem, 7vw, 1.9rem);
  }

  .hero__lead,
  .lead {
    font-size: 0.98rem;
    line-height: 1.7;
  }

  /* Full-width, thumb-friendly buttons */
  .hero__actions,
  .shared-card .btn-row {
    display: grid;
    grid-template-columns: 1fr;
    gap: 0.7rem;
  }

  .hero__actions .btn,
  .cta-panel .btn,
  .dl-card__actions .btn {
    width: 100%;
  }

  .btn--lg {
    padding: 1.05rem 1.5rem;
    font-size: 0.98rem;
  }

  .hero__meta {
    gap: 0.45rem 1rem;
    margin-top: 2rem;
    font-size: 0.62rem;
  }

  .hero__visual .phone {
    --phone-w: 235px;
  }

  .hero__visual::before {
    inset: -6% -12%;
  }

  .orb--one,
  .orb--two,
  .page-hero .orb--one {
    width: 16rem;
    height: 16rem;
    right: -9rem;
  }

  .screens {
    gap: 2.25rem;
  }

  .screen .phone {
    --phone-w: 230px;
  }

  .principles,
  .steps,
  .standards {
    gap: 0.9rem;
  }

  .principle h3 {
    font-size: 1.12rem;
  }

  .principle__icon,
  .step__icon {
    width: 40px;
    height: 40px;
  }

  .quote blockquote {
    font-size: clamp(1.4rem, 6.5vw, 1.8rem);
  }

  .cta-panel {
    padding: 1.75rem 1.2rem;
    border-radius: 1.2rem;
  }

  .badges {
    gap: 0.45rem;
  }

  .badge {
    font-size: 0.62rem;
    padding: 0.36rem 0.7rem;
  }

  .shared__main {
    padding-top: calc(var(--header-h) + 1.75rem);
  }

  .shared__inner {
    width: min(680px, 100% - 1.5rem);
    gap: 1.1rem;
  }

  .shared-card h1 {
    font-size: 1.35rem;
  }

  .shared-card__icon {
    width: 54px;
    height: 54px;
  }

  .post-preview {
    padding: 1.2rem 1.1rem 1.3rem;
  }

  .peek {
    padding: 1.4rem 1.1rem;
  }

  .peek .phone {
    --phone-w: 170px;
  }

  .creator__card {
    padding: 1.6rem 1.2rem;
  }

  .creator__bio .highlight {
    font-size: 0.98rem;
  }

  .form-row {
    gap: 0;
  }

  .field input,
  .field select,
  .field textarea {
    /* ≥16px stops iOS Safari from auto-zooming into inputs */
    font-size: 1rem;
    padding: 0.8rem 0.9rem;
  }

  .site-footer__bottom {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.45rem;
  }
}

/* Very small phones (≤360px) --------------------------------- */
@media (max-width: 360px) {
  .display {
    font-size: 2rem;
  }

  .hero__visual .phone,
  .screen .phone {
    --phone-w: 210px;
  }
}

/* ---------- 22. Utility ---------- */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

[hidden] {
  display: none !important;
}

/* ---------- 23. Light theme effect overrides ----------------------------
   Alphas tuned against near-black are far too heavy on white: a 0.9 violet
   bloom reads as a bruise rather than a glow. These are the ONLY per-site
   overrides the light theme needs — everything else follows the channels in
   section 1h. Placed last so they win the cascade against the component
   rules they tune (the responsive section declares no box-shadow and none
   of these selectors, so nothing is clobbered).                        */
@media (prefers-color-scheme: light) {
  /* Small emissive dots keep their bloom, just dialed back to a halo. */
  .eyebrow__dot {
    box-shadow: 0 0 10px rgba(var(--glow-rgb), 0.35);
  }

  .site-footer__signal i {
    box-shadow: 0 0 8px rgba(var(--glow-rgb), 0.4);
  }

  /* The ambient toggle carried a 0.8-0.9 bloom; on white that is a bruise,
     so it becomes a crisp diffused shadow that still lifts the chip.   */
  .ambient-toggle {
    box-shadow: 0 10px 24px -14px rgba(var(--shadow-rgb), 0.32);
  }

  .ambient-toggle:hover,
  .ambient-toggle:focus-visible,
  .ambient-toggle.is-on {
    box-shadow: 0 12px 28px -14px rgba(var(--shadow-rgb), 0.38);
  }

  /* Small print at 0.56 ink is 5.9:1 on near-black but only 3.87:1 on
     off-white. --text-faint clears AA on every surface the credits screen
     actually paints (5.02:1 worst case) and stays fainter than the 0.88
     caption above it, so the hierarchy survives the theme switch. */
  .rotary-profile__pending small {
    color: var(--text-faint);
  }

  /* The hanging role sign kept a 0.88 blue bloom under it, which on white reads
     as a bruise; this rule sits later in the cascade than the shared one, so it
     is the value that actually applies in light mode. */
  .rotary-name-tag {
    box-shadow: 0 10px 28px -14px rgba(var(--glow-blue-rgb), 0.4),
      0 0 24px rgba(var(--glow-rgb), 0.16);
  }
}

/* Privacy and Terms open on a plain .section instead of .page-hero, so their
   top padding was the section default -- and on a phone that is barely more
   than the fixed header, leaving the eyebrow two pixels under the bar. Give
   them the same header allowance every other page gets. */
.privacy-page > .section {
  padding-top: calc(var(--header-h) + clamp(1.75rem, 4vw, 3rem));
}

/* Privacy: the shared shell with a calm, readable legal-document measure. */
.privacy-layout { max-width: 900px; }
.privacy-heading { margin-bottom: 2.5rem; }
.privacy-date { color: var(--text-dim); margin-block: 1rem; }
.privacy-intro { max-width: 65ch; }
.privacy-contents {
  padding: clamp(1.25rem, 3vw, 2rem);
  margin-bottom: 3rem;
  border: 1px solid var(--border);
  border-radius: 16px;
  background: var(--surface);
}
.privacy-contents ol { padding-left: 1.5rem; margin-top: 1rem; }
.privacy-contents li { padding-block: .3rem; }
.privacy-copy { max-width: 72ch; line-height: 1.85; overflow-wrap: anywhere; }
.privacy-copy section { scroll-margin-top: 7rem; margin-bottom: 3rem; }
.privacy-copy h2 { font-size: clamp(1.35rem, 3vw, 1.8rem); line-height: 1.35; margin-bottom: 1rem; }
.privacy-copy h3 { font-size: 1.1rem; margin-block: 1.5rem .65rem; }
.privacy-copy p, .privacy-copy ul { margin-bottom: 1rem; }
.privacy-copy ul { padding-left: 1.4rem; }
.privacy-copy li { padding-left: .25rem; margin-block: .5rem; }
.privacy-copy a, .privacy-contents a { text-decoration: underline; text-underline-offset: .2em; }
.privacy-copy code { font-size: .9em; }
.credits-screen-colophon a { margin-left: .75em; text-decoration: underline; text-underline-offset: .2em; }

/* Keep the new legal exit operable without intercepting wheel interactions. */
.credits-screen-colophon a { pointer-events: auto; display: inline-block; padding-block: .3rem; }
@media (max-width: 768px) {
  .credits-screen-colophon { white-space: normal; }
  /* Inline, so the two legal links share the colophon line instead of adding
     two more rows to the compact status strip. */
  .credits-screen-colophon a {
    display: inline;
    margin-left: 0.5em;
    padding-block: 0;
  }
}
.privacy-copy {
  padding: clamp(1rem, 3vw, 2rem);
  border-radius: 16px;
  background: rgba(var(--bg-rgb), 0.74);
}
.privacy-heading .display { font-size: clamp(2.25rem, 6vw, 4rem); }
.privacy-contents a:focus-visible, .privacy-copy a:focus-visible,
.credits-screen-colophon a:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
