@layer reset, tokens, base, utilities, components, page;

/* ============================================================
   DESIGN TOKENS — black studio / neon ink
   Colours are declared in oklch() (source of truth — Prompt 3/4
   read these) with an sRGB fallback for browsers predating
   oklch() support. Same hues as the original hex literals below;
   not re-picked — see the fallback block for the exact hex.
   ============================================================ */
@layer tokens{
:root{
  /* Near-black ground, warm-white ink, three neons at FULL saturation —
     no white dilution, they should read like lit signage. Lime is the
     brand accent; cyan and magenta only appear in assigned roles (indices,
     sparks, monograms) so the colour reads deliberate, not random. */
  --bg0: oklch(14.5% 0.002 286.1);
  --bg1: oklch(18.4% 0.008 285.6);
  --bg2: oklch(22.0% 0.012 285.4);
  --fg:   oklch(95.9% 0.008 106.5);
  --fg-2: oklch(73.4% 0.011 286.1);
  --fg-3: oklch(60.0% 0.017 285.9);   /* ≈5.0:1 on bg0 / ≈4.7:1 on bg1 — clears WCAG AA for the small text it colours */

  --lime:    oklch(93.1% 0.229 123.1);
  --cyan:    oklch(87.0% 0.148 202.9);
  --magenta: oklch(68.6% 0.293 337.2);
  --on-lime: oklch(17.5% 0.029 115.5);  /* ink used on lime surfaces */
  --ok:  oklch(81.0% 0.214 151.8);
  --err: oklch(67.9% 0.209 24.7);
  --sun: oklch(81.2% 0.148 81.0);      /* demo-internal highlight — client-site world only */

  --line:   rgba(242,242,236,.10);
  --line-2: rgba(242,242,236,.22);

  /* --color-* semantic aliases (required token family). The primitives
     above already ARE this page's colour roles under their own names
     (bg0/bg1/bg2/fg/lime/…), used in ~900 places throughout this
     stylesheet — aliased here rather than mechanically renamed, so the
     semantic --color-* vocabulary exists for Prompt 3/4 without a
     high-risk find/replace across the whole file. */
  --color-surface: var(--bg0);
  --color-surface-2: var(--bg1);
  --color-surface-3: var(--bg2);
  --color-on-surface: var(--fg);
  --color-on-surface-muted: var(--fg-2);
  --color-on-surface-subtle: var(--fg-3);
  --color-primary: var(--lime);
  --color-on-primary: var(--on-lime);
  --color-accent-cyan: var(--cyan);
  --color-accent-magenta: var(--magenta);
  --color-success: var(--ok);
  --color-danger: var(--err);
  --color-highlight: var(--sun);
  --color-border: var(--line);
  --color-border-strong: var(--line-2);

  /* Type */
  --font-display:'Archivo', ui-sans-serif, system-ui, sans-serif;
  --font-serif:'Fraunces', Georgia, 'Times New Roman', serif;
  --font-body:'Instrument Sans', ui-sans-serif, system-ui, sans-serif;
  --font-mono:'Spline Sans Mono', ui-monospace, Menlo, monospace;

  /* Radius */
  --radius-sm:8px; --radius-md:12px; --radius-lg:20px; --radius-xl:28px;
  --radius-pill:999px; --radius-full:9999px;

  /* Space — 4px step */
  --space-1:4px; --space-2:8px; --space-3:12px; --space-4:16px; --space-5:24px;
  --space-6:32px; --space-7:48px; --space-8:64px; --space-9:96px; --space-10:128px;

  /* Shadow — deep neutral blacks; lift comes from borders + surface steps */
  --shadow-sm:0 1px 2px rgba(0,0,0,.5);
  --shadow-md:0 16px 40px -16px rgba(0,0,0,.65);
  --shadow-lg:0 40px 90px -30px rgba(0,0,0,.8);

  /* Motion — the page's own tuned vocabulary (used ~40+ places below),
     kept verbatim, PLUS the prompt's mandatory-minimum named vocabulary
     alongside it (new infrastructure for Prompt 3/4 and for tokens
     introduced this pass — rings, shimmer). Both co-exist deliberately;
     see the CHANGELOG "Tokenisation" note. */
  --ease-out:cubic-bezier(.2,.7,.2,1);
  --ease-in-out:cubic-bezier(.65,.05,.36,1);
  --dur-1:140ms; --dur-2:260ms; --dur-3:550ms;

  --ease-out-quad:   cubic-bezier(0.5, 1, 0.89, 1);
  --ease-out-expo:   cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring:     cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
  --duration-fast: 120ms;
  --duration-base: 240ms;
  --duration-slow: 480ms;

  /* Z-index — page-level roles only (a handful of small, purely
     LOCAL z-indices elsewhere — e.g. the compare-slider's internal
     layers, the hero pins/orbit — stay as literals: they order
     siblings inside their own component's stacking context and have
     no relationship to this global scale; forcing them onto it would
     be organisational noise, not a real fix). --z-chrome/--z-skip
     extend the mandatory minimum upward for the two fixed, page-wide
     layers (film-grain overlay, skip-link) that must win over
     everything, including the mandatory scale's own --z-tooltip. */
  --z-base:     0;
  --z-dropdown: 100;
  --z-sticky:   200;
  --z-overlay:  300;
  --z-modal:    400;
  --z-toast:    500;
  --z-tooltip:  600;
  --z-chrome:   2000;
  --z-skip:     2100;

  /* Blur */
  --blur-sm: 6px;
  --blur-md: 12px;
  --blur-lg: 24px;

  /* Gradients — built from the page's own original literal values
     (never re-picked); --gradient-shimmer is new infrastructure for
     any future async/skeleton state (this page's regions render their
     final markup directly, so none currently need it, but the rule
     ships per the prompt's mandatory pattern). */
  --gradient-hero: radial-gradient(58% 46% at 68% 0%, rgba(204,255,0,.06), transparent 68%);
  --gradient-shimmer: linear-gradient(90deg, var(--bg1) 0%, color-mix(in oklch, var(--bg1) 60%, white) 50%, var(--bg1) 100%);
  --gradient-aura-lime:    radial-gradient(56% 300px at 50% 0%, rgba(204,255,0,.04), transparent 70%);
  --gradient-aura-cyan:    radial-gradient(56% 300px at 50% 0%, rgba(0,240,255,.05), transparent 70%);
  --gradient-aura-magenta: radial-gradient(56% 300px at 50% 0%, rgba(255,26,217,.05), transparent 70%);
  --gradient-sheen-strong: linear-gradient(105deg, transparent 42%, rgba(255,255,255,.28) 50%, transparent 58%);
  --gradient-sheen-soft:   linear-gradient(105deg, transparent 43%, rgba(255,255,255,.18) 50%, transparent 57%);
  /* Scrims — the readability wash under a caption sitting on a photograph.
     Added 2026-08-11. The gallery cards had these inline as rgba(10,10,12,…),
     which is not --bg0: the ground is #0a0a0b, so the blue channel was one
     step off. Eyeballed, not tokenised — exactly the drift a page picks up
     when it is authored beside the design system instead of from it. */
  --gradient-scrim:      linear-gradient(to top, color-mix(in srgb, var(--bg0) 92%, transparent),
                                                color-mix(in srgb, var(--bg0) 68%, transparent) 55%,
                                                transparent);
  --gradient-scrim-soft: linear-gradient(to top, color-mix(in srgb, var(--bg0) 90%, transparent),
                                                transparent);

  /* Focus rings — colour kept fully opaque (not the spec example's
     50%-transparent color-mix) to match this page's own already-
     audited high-contrast focus treatment (2026-07-04 a11y pass). */
  --ring-color: var(--lime);
  --ring-color-inverse: var(--on-lime); /* the two lime surfaces need the dark ink, not lime-on-lime */
  --ring-offset: 3px;
  --ring-width: 2px;

  --measure:64ch;
  --maxw:1200px;
  --gutter:clamp(20px, 5vw, 56px);

  color-scheme: dark;
}

/* This register is dark-native by construction (color-scheme:dark; true
   black surfaces are the signature, not an incomplete light mode) — see
   the CHANGELOG "Tokenisation" note for why a full alternate light
   palette is deliberately NOT introduced here. The override block below
   is declared (mirroring :root) so [data-theme="dark"] is real,
   reachable infrastructure for a future theme toggle. */
[data-theme="dark"]{
  --bg0: oklch(14.5% 0.002 286.1); --bg1: oklch(18.4% 0.008 285.6); --bg2: oklch(22.0% 0.012 285.4);
  --fg: oklch(95.9% 0.008 106.5); --fg-2: oklch(73.4% 0.011 286.1); --fg-3: oklch(60.0% 0.017 285.9);
  --lime: oklch(93.1% 0.229 123.1); --cyan: oklch(87.0% 0.148 202.9); --magenta: oklch(68.6% 0.293 337.2);
  --on-lime: oklch(17.5% 0.029 115.5); --ok: oklch(81.0% 0.214 151.8); --err: oklch(67.9% 0.209 24.7);
  --sun: oklch(81.2% 0.148 81.0);
}

/* sRGB fallback for browsers without oklch() support (pre Safari 15.4 /
   Chrome 111 / Firefox 113) — the ORIGINAL hex literals, verbatim. */
@supports not (color: oklch(0 0 0)){
  :root{
    --bg0:#0a0a0b; --bg1:#121216; --bg2:#1a1a20;
    --fg:#f2f2ec; --fg-2:#a8a8b0; --fg-3:#7f7f8a;
    --lime:#ccff00; --cyan:#00f0ff; --magenta:#ff1ad9;
    --on-lime:#101204; --ok:#00e676; --err:#ff5252; --sun:#f2b63c;
  }
  [data-theme="dark"]{
    --bg0:#0a0a0b; --bg1:#121216; --bg2:#1a1a20;
    --fg:#f2f2ec; --fg-2:#a8a8b0; --fg-3:#7f7f8a;
    --lime:#ccff00; --cyan:#00f0ff; --magenta:#ff1ad9;
    --on-lime:#101204; --ok:#00e676; --err:#ff5252; --sun:#f2b63c;
  }
}
} /* /@layer tokens */

/* ============================================================
   RESET
   ============================================================ */
@layer reset{
*,*::before,*::after{box-sizing:border-box}
html,body{margin:0}
}

/* ============================================================
   BASE — global element defaults, skeleton-shimmer infrastructure,
   and every site-wide @keyframes definition (KEYFRAME PLACEMENT:
   ALL @keyframes live here regardless of which constituent uses
   them, so component/page CSS only ever references animation names).
   ============================================================ */
@layer base{
html{scroll-behavior:smooth; scroll-padding-top:96px; background:var(--bg0)}
body{
  margin:0;
  font-family:var(--font-body);
  background:var(--bg0);
  color:var(--fg);
  font-size:16px; line-height:1.65;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
  overflow-x:hidden;
}
/* Film grain — white noise at 6%, so the black never reads like a dead #000 fill */
body::after{
  content:""; position:fixed; inset:0; z-index:var(--z-chrome); pointer-events:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.06 0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}
h1,h2,h3,h4{
  font-family:var(--font-display); font-stretch:106%;
  line-height:1.05; letter-spacing:-0.024em; margin:0; font-weight:760;
  text-wrap:balance;
}
p{margin:0; text-wrap:pretty}
a{color:inherit; text-decoration:none}
img,svg{display:block; max-width:100%}
button{font-family:inherit; cursor:pointer; border:none; background:none; color:inherit}
ul{margin:0; padding:0; list-style:none}
::selection{background:var(--lime); color:var(--on-lime)}
:focus-visible{outline:var(--ring-width) solid var(--ring-color); outline-offset:var(--ring-offset); border-radius:4px}
/* on the two lime surfaces a lime ring is invisible — swap it for the dark ink */
.plan.featured :focus-visible,
.cta-show :focus-visible{outline-color:var(--ring-color-inverse)}

/* Skeleton shimmer — mandatory infrastructure for any island that ever
   renders a generic loading placeholder. This page has zero islands, so
   nothing currently uses it, but the rule ships as site-wide
   infrastructure per the prompt's mandatory pattern. */
@keyframes shimmer{ from{background-position:200% 0} to{background-position:-200% 0} }
.is-skeleton,
[class$="-skeleton"] > *{
  background-image:var(--gradient-shimmer);
  background-size:200% 100%;
  background-repeat:no-repeat;
  animation:shimmer var(--duration-slow) linear infinite;
  border-radius:var(--radius-sm);
  color:transparent;
}

/* Hero swash-in underline (hero.html) */
@keyframes swash-in{to{background-size:100% .16em}}
/* Hero drafting-orbit viz rings/ping/twinkle (hero.html) */
@keyframes viz-spin{to{transform:rotate(360deg)}}
@keyframes viz-ping{
  0%{transform:scale(.42); opacity:.5}
  70%{opacity:0}
  100%{transform:scale(2); opacity:0}
}
@keyframes viz-twinkle{
  0%,100%{opacity:.12; transform:scale(.8) rotate(0deg)}
  50%{opacity:.9; transform:scale(1.15) rotate(45deg)}
}
/* Hero signature sticker rotation (hero.html) */
@keyframes orbit-spin{to{transform:rotate(360deg)}}
/* Proof marquee scroll (components.css: proof-bar) */
@keyframes proof-marquee{from{transform:translateX(0)} to{transform:translateX(-50%)}}

/* Ambient-tier keyframes (item 9 idle loops) — each runs on an element
   carrying .is-ambient, paused offscreen by .is-ambient.is-paused below. */
@keyframes amb-spin{to{transform:rotate(360deg)}}
@keyframes amb-caret{0%,54%{opacity:.85} 55%,100%{opacity:.12}}
@keyframes amb-scan{0%{transform:translateX(-280px)} 58%,100%{transform:translateX(calc(100vw + 40px))}}
@keyframes amb-ink{0%,100%{-webkit-text-stroke-color:var(--line-2)} 50%{-webkit-text-stroke-color:var(--ink,var(--line-2))}}
@keyframes amb-ping{0%{transform:scale(.55); opacity:0} 8%{opacity:.5} 26%,100%{transform:scale(1.8); opacity:0}}
@keyframes amb-drift{0%,100%{translate:0 0} 50%{translate:0 -5px}}
@keyframes amb-bob{0%,100%{transform:rotate(-2.6deg) translateY(0)} 50%{transform:rotate(-1.1deg) translateY(-4px)}}
@keyframes amb-star{0%,100%{opacity:.65; transform:scale(.94)} 50%{opacity:1; transform:scale(1.12)}}
@keyframes amb-sheen{0%{background-position:200% 0} 24%,100%{background-position:-80% 0}}
@keyframes amb-pulse{0%,100%{transform:scale(1)} 50%{transform:scale(1.08)}}
@keyframes amb-nudge{0%,58%,86%,100%{translate:0 0} 72%{translate:4px 0}}
.is-ambient.is-paused{
  animation-play-state: paused;
}
}

/* ============================================================
   scroll-reveal infrastructure (animation-timeline path is
   progressive enhancement — the IntersectionObserver in
   00_base.html drives the fallback for older browsers).
   ALL pre-reveal hiding is scoped to `html.js` — the inline
   <script> in 00_base.html adds that class, so with JS disabled
   nothing is hidden and the page reads in full. Mandatory
   site-wide infrastructure (see CHANGELOG "data-reveal note"):
   this page's own reveal system is .reveal/.reveal.in/[data-delay]
   (see components.css / <NN_page>.css + site-interactions.js);
   no block on this page carries [data-reveal], so these rules are
   currently unused but ship per the prompt's own "always emit"
   instruction.
   ============================================================ */
@layer base{
html.js [data-reveal] {
  opacity: 0;
  transition:
    opacity var(--duration-slow, 600ms) var(--ease-out-expo, ease-out),
    transform var(--duration-slow, 600ms) var(--ease-out-expo, ease-out),
    filter var(--duration-slow, 600ms) var(--ease-out-expo, ease-out);
  will-change: opacity, transform, filter;
  /* honour stagger index when nested inside a stagger parent */
  transition-delay: calc(var(--reveal-index, 0) * 80ms);
}

html.js [data-reveal],
html.js [data-reveal="fade-up"] {
  transform: translateY(16px);
}
html.js [data-reveal="fade"] {
  transform: none;
}
html.js [data-reveal="scale-in"] {
  transform: scale(0.96);
  transform-origin: center;
}
html.js [data-reveal="slide-right"] {
  transform: translateX(-24px);
}
html.js [data-reveal="blur-in"] {
  transform: translateY(8px);
  filter: blur(8px);
}

html.js [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
  filter: none;
}

[data-reveal-stagger] > [data-reveal] {
  transition-delay: calc(var(--reveal-index, 0) * 80ms);
}

@supports (animation-timeline: view()) {
  [data-reveal][data-reveal-repeat]:not(.is-revealed) {
    animation: reveal-from-view linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 30%;
  }
  @keyframes reveal-from-view {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
}
}

/* ============================================================
   UTILITIES — a11y helpers + layout primitives used site-wide
   ============================================================ */
@layer utilities{
.skip{position:absolute; left:-999px; top:8px; background:var(--lime); color:var(--on-lime);
  padding:10px 16px; border-radius:8px; z-index:var(--z-skip); font-weight:600}
.skip:focus{left:12px}
/* real screen-reader-only utility (was: abusing .skip for the newsletter label) */
.sr-only{position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden;
  clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0}

.wrap{max-width:var(--maxw); margin-inline:auto; padding-inline:var(--gutter)}
.section{padding-block:var(--space-9); border-top:1px solid var(--line)}
/* ambient section auras — one whisper of each section's index neon bleeding
   down from its top hairline, so the page carries colour between the inks.
   The landing page's sections are addressed by id because their order is
   fixed and hand-authored; the galleries' bands are generated by a loop, so
   they carry the same .sx-* class that already drives their eyebrow hue and
   pick the matching aura up from it. Two selectors, one rule set, because
   the two pages know their sections in genuinely different ways. */
#how,      .family.sx-lime{background:var(--gradient-aura-lime)}
#included, .family.sx-cyan{background:var(--gradient-aura-cyan)}
#features, .family.sx-magenta{background:var(--gradient-aura-magenta)}
#work{background:var(--gradient-aura-lime)}
#pricing{background:var(--gradient-aura-cyan)}
#testimonials{background:var(--gradient-aura-magenta)}
#faq{background:var(--gradient-aura-lime)}
.eyebrow{--sx:var(--lime); font-family:var(--font-mono); font-size:.72rem; letter-spacing:.18em; text-transform:uppercase;
  color:var(--fg-3); display:inline-flex; align-items:center; gap:.9em; font-weight:600}
.eyebrow i{font-style:normal; color:var(--sx); text-shadow:0 0 12px}  /* colourless shadow = currentColor, so each section's neon glows its own hue */
/* every section index ticks like a terminal caret, in its section's neon —
   ambient trigger for the .eyebrow utility above (amb-caret defined in
   @layer base); co-located here since it is part of the same utility class */
.eyebrow i::after{content:""; display:inline-block; width:.5em; height:.95em;
  margin-left:.45em; vertical-align:-.12em; background:currentColor;
  animation:amb-caret 1.25s linear infinite}
/* section indices cycle the three neons: 01 lime · 02 cyan · 03 magenta · …
   (--sx also tints the eyebrow's leading dash) */
#included .eyebrow{--sx:var(--cyan)}
#features .eyebrow{--sx:var(--magenta)}
#pricing .eyebrow{--sx:var(--cyan)}
#testimonials .eyebrow{--sx:var(--magenta)}
.eyebrow::before{content:""; width:26px; height:1px;
  background:color-mix(in srgb, var(--sx) 60%, transparent)}
.section-head{max-width:62ch}
.section-head h2{font-size:clamp(2.1rem, 4.2vw, 3.3rem); margin-top:var(--space-5)}
.section-head p{color:var(--fg-2); font-size:1.12rem; margin-top:var(--space-4); max-width:56ch}
.lede{color:var(--fg-2)}
} /* /@layer utilities */

/* ============================================================
   SHARED WIDGETS — rules used by constituents that ship on EVERY
   page (components/navbar.html, components/footer.html), so they
   cannot live in any one page entry's extra_styles.

   These were authored under @layer components upstream and STAY
   under @layer components here. Do not re-wrap them in @layer page:
   layer order outranks specificity unconditionally, so a relabelled
   .btn would beat every components-layer override site-wide —
   e.g. navbar.html's `.nav-cta .btn{min-height:44px}` and its
   mobile `.nav-cta .btn-ghost{display:none}`.

   Moved here from 01_landing.html's extra_styles on 2026-08-03,
   when 02_themes.html became the second page including the navbar.
   ============================================================ */
@layer components{
/* ============================================================
   BUTTONS
   ============================================================ */
.btn{display:inline-flex; align-items:center; justify-content:center; gap:.55em;
  min-height:50px; padding:0 var(--space-6); border-radius:var(--radius-pill);
  font-weight:600; font-size:.97rem; letter-spacing:-.005em;
  border:1px solid transparent; touch-action:manipulation;
  transition:background var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out),
             border-color var(--dur-2) var(--ease-out), transform var(--dur-1) var(--ease-out),
             box-shadow var(--dur-2) var(--ease-out)}
.btn svg{width:17px; height:17px; transition:transform var(--dur-2) var(--ease-out)}
.btn:hover svg{transform:translateX(3px)}
.btn-primary{background:var(--lime); color:var(--on-lime); border-color:var(--lime);
  box-shadow:0 0 28px -6px rgba(204,255,0,.55)}
.btn-primary:hover{background:var(--fg); border-color:var(--fg); transform:translateY(-1px); box-shadow:var(--shadow-md)}
.btn-primary:active{transform:translateY(0)}
.btn-ghost{border-color:var(--line-2); color:var(--fg); background:transparent}
.btn-ghost:hover{background:rgba(204,255,0,.08); border-color:var(--lime); color:var(--lime); transform:translateY(-1px)}
/* CTA-context button — lives on the lime showcase, so it inverts to black */
.btn-gold{background:var(--bg0); color:var(--lime); border-color:var(--bg0)}
.btn-gold:hover{color:#fff; transform:translateY(-1px); box-shadow:0 14px 30px -12px rgba(16,18,4,.55)}
.btn-lg{min-height:58px; padding:0 var(--space-7); font-size:1.03rem}

.link-arrow{color:var(--lime); font-weight:600; display:inline-flex; align-items:center; gap:.4em}
.link-arrow svg{width:16px; height:16px; transition:transform var(--dur-1) var(--ease-out)}
.link-arrow:hover svg{transform:translateX(4px)}
/* ============================================================
   THEME GALLERY WIDGETS — shared by 03_themes_hero.html and
   04_themes_landing.html (and .chip by 02_themes.html too).

   Here rather than in either page's extra_styles for the same
   reason as the buttons above: more than one page uses them, so
   no single page entry can own them. Both galleries are the same
   tool and must read as one — a card that drifts between /themes/hero
   and /themes/landing is a bug, not a variation.

   @layer components so a page CAN override a detail in its own
   @layer page block without fighting specificity.
   ============================================================ */
.chip{display:inline-flex; align-items:center; padding:4px 10px;
  border:1px solid var(--line-2); border-radius:var(--radius-pill);
  font-family:var(--font-mono); font-size:.72rem; letter-spacing:.08em;
  text-transform:uppercase; color:var(--fg-3)}
.chip.is-live{color:var(--lime); border-color:color-mix(in srgb, var(--lime) 40%, transparent)}

/* ---- scroll reveal ----------------------------------------------
   RESTORED 2026-08-11, and this is a bug fix, not a feature.

   These five rules were authored in the monolith under @layer page
   (templates/app3/archive/landing.html, "SCROLL REVEAL" banner) and were
   LOST when the page was decomposed — they landed in no block, no page
   entry and not here, so the only surviving copy was the archived
   monolith. The JS survived, in 01_landing.html's extra_scripts, and has
   been happily adding .in to elements that had no hidden state to
   transition out of. Every .reveal on the live landing page has been
   inert since decomposition: 33 elements, no fade, no rise.

   Here rather than in a page entry because four pages now carry .reveal
   (the landing's blocks plus the three galleries' section heads and
   cards), which is the MULTI-PAGE case — the same reason .btn and the
   .theme-* widgets moved here.

   Scoped to html.js, matching the [data-reveal] infrastructure below: the
   inline script in 00_base.html sets that class, so with JS disabled
   nothing is hidden and the page reads in full. The monolith's version
   was NOT scoped this way — with JS off it left the page permanently
   blank. Fixed in passing.

   prefers-reduced-motion neutralises all of it at the bottom of this
   file, outside every layer, where the !important rules win regardless. */
html.js .reveal{opacity:0; transform:translateY(18px);
  transition:opacity var(--dur-3) var(--ease-out), transform var(--dur-3) var(--ease-out)}
html.js .reveal.in{opacity:1; transform:none}
.reveal[data-delay="1"]{transition-delay:80ms}
.reveal[data-delay="2"]{transition-delay:160ms}
.reveal[data-delay="3"]{transition-delay:240ms}

/* ---- card shell -------------------------------------------------
   The surface every card on the site sits on, and the lift it does when
   it is a link. Extracted 2026-08-11: .theme-card (the gallery cards) and
   .gallery-card (the /themes index + external references) had grown two
   independent copies of the same six declarations and the same hover, and
   they had already drifted — one used var(--shadow-md), the other a raw
   `0 18px 40px -24px`, and is-pending was .5 opacity in one file and .55
   in the other. Now they extend this and keep only what makes them
   different: .theme-card its overflow-clipped screenshot, .gallery-card
   its flex column and padding.

   The markup stays separate on purpose — a card that wraps a 16:10 image
   and a card that wraps a heading + prose are not one component with a
   flag. What was shared was the surface, so the surface is what moved. */
.card{position:relative; border:1px solid var(--line); border-radius:var(--radius-lg);
  background:var(--bg1);
  transition:border-color var(--dur-2) var(--ease-out),
             transform var(--dur-2) var(--ease-out),
             box-shadow var(--dur-2) var(--ease-out)}
a.card:hover{border-color:var(--lime); transform:translateY(-3px); box-shadow:var(--shadow-md)}
/* not built / not generated yet — reads as inert, not as a broken link */
.card.is-pending{opacity:.5; border-style:dashed}

/* ---- page header shared by both galleries ---- */
.theme-head{padding-block:var(--space-9) var(--space-7)}
.theme-head h1{font-size:clamp(2.6rem, 6vw, 4.4rem); line-height:1.03;
  letter-spacing:-.02em; margin-top:var(--space-5)}
.theme-head p{color:var(--fg-2); font-size:1.12rem; margin-top:var(--space-4); max-width:60ch}
.theme-head .lede-meta{margin-top:var(--space-5); display:flex; flex-wrap:wrap; gap:var(--space-4)}

/* ---- family band ---- */
.family{padding-block:var(--space-8)}
.family .section-head{margin-bottom:var(--space-6)}
/* The three neons cycle down the bands, matching the landing page's
   per-section index colouring. Driven by a cycled class rather than
   :nth-child — the bands are not main's only children (the page header
   is child 1), so nth-child would be off by one. */
.family.sx-cyan .eyebrow{--sx:var(--cyan)}
.family.sx-magenta .eyebrow{--sx:var(--magenta)}

/* ---- grid: 3-up desktop so each family is one clean row ---- */
.theme-grid{display:grid; gap:var(--space-5); grid-template-columns:1fr}
@media (min-width:640px){ .theme-grid{grid-template-columns:repeat(2,1fr)} }
@media (min-width:1024px){ .theme-grid{grid-template-columns:repeat(3,1fr)} }

/* ---- card ---- (surface + hover come from .card above) */
.theme-card{display:block; overflow:hidden}

.theme-shot{position:relative; aspect-ratio:16/10; overflow:hidden;
  /* the gradient sits UNDER the screenshot, so a card whose .webp has not
     been captured yet still reads as a deliberate surface rather than a
     broken image */
  background-color:var(--bg2);
  background-image:linear-gradient(140deg,
    color-mix(in srgb, var(--lime) 12%, transparent),
    color-mix(in srgb, var(--cyan) 10%, transparent) 55%,
    transparent)}
.theme-shot img{position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  object-position:top center; transition:transform var(--dur-3) var(--ease-out)}
a.theme-card:hover .theme-shot img{transform:scale(1.03)}

.theme-caption{position:absolute; inset-inline:0; bottom:0; z-index:1;
  padding:var(--space-5) var(--space-5) var(--space-4);
  background:var(--gradient-scrim)}
.theme-caption .t{font-family:var(--font-display); font-weight:600;
  font-size:1.12rem; letter-spacing:-.01em; color:var(--fg)}
.theme-caption .r{margin-top:4px; font-family:var(--font-mono); font-size:.74rem;
  letter-spacing:.04em; color:var(--fg-3)}

.theme-purpose{padding:var(--space-4) var(--space-5) var(--space-5);
  color:var(--fg-2); font-size:.92rem; line-height:1.5}

/* not generated yet — .card.is-pending dims and dashes it; this is the
   caption's own adjustment on top */
.theme-card.is-pending .theme-caption{background:var(--gradient-scrim-soft)}
.pending-tag{position:absolute; top:var(--space-4); right:var(--space-4); z-index:2;
  background:var(--bg0); border:1px solid var(--line-2); border-radius:var(--radius-pill);
  padding:3px 9px; font-family:var(--font-mono); font-size:.68rem;
  letter-spacing:.08em; text-transform:uppercase; color:var(--fg-3)}
.default-tag{color:var(--lime); border-color:color-mix(in srgb, var(--lime) 45%, transparent)}
} /* /@layer components (buttons, link-arrow, theme gallery) */

/* ============================================================
   REDUCED MOTION — top-level, OUTSIDE any layer, so its
   !important rules win unconditionally regardless of layer order.
   Preserves opacity/colour transitions (reduces cognitive load)
   while killing every spatial-motion property; the belt-and-braces
   block additionally resets transform/translate/scale/rotate to
   none on anything still carrying a reveal/animation/ambient class
   (pseudo-element ambient loops — ::after/::before sheens, carets,
   pings — are already caught by the universal *,*::before,*::after
   rule above, since generated content can't carry a class of its own).
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-property: opacity, background-color, color, border-color,
                         box-shadow, fill, stroke !important;
    transition-duration: var(--duration-fast, 120ms) !important;
    scroll-behavior: auto !important;
  }
  [data-reveal],
  .is-animated,
  .is-revealed,
  .is-ambient,
  .reveal {
    transform: none !important;
    translate: none !important;
    scale: none !important;
    rotate: none !important;
  }
  .reveal{opacity:1}
  .hero h1 .accent{background-size:100% .16em}
  .eyebrow i::after{display:none}  /* a frozen caret would read as a stray block */
}

