/* ============================================================================
   FrunkShow — frunkshow.keocodes.com
   "The instrument, at night."

   FrunkShow is sequencer software for a car in a dark driveway, so the page is
   built like a piece of pro tooling: a dark ground, a neutral surface stack,
   and light as the only expressive material. Four motifs carry the whole site.

     1. THE SPECTRUM RAIL — a 2px line carrying the full spectrum. It is the
        scroll-progress bar on the header, the underline on the active nav item
        and the hairline above the footer. A LINE, never a filled area. This is
        the nod to Rainbow Road in Tesla's Toybox.
     2. CHANNEL LANES — hairlines with evenly spaced ticks, drawn with
        repeating-linear-gradient (never images). A light show is 48 channels on
        a 20ms grid; the dividers say so.
     3. ILLUMINATION, NOT ELEVATION — nothing casts a drop shadow. Interactive
        things light up: the border brightens and a coloured bloom appears
        behind. Cards take different spectrum hues in sequence, so a set reads
        as a spectrum without any single card being "the accent one".
     4. DATA IN MONO — timecodes, channel numbers, byte counts and filenames are
        JetBrains Mono with tabular figures. That is where the precision shows.

   DARK ONLY. There is no light variant, no prefers-color-scheme block and no
   [data-theme] branch anywhere in this file, by design.

   Mobile-first: the phone layout is the base, enhanced at 720px and 1040px.
   ========================================================================== */

/* ── Fonts ───────────────────────────────────────────────────────────────────
   Both faces are variable and self-hosted under /assets/fonts (CSP font-src is
   'self'; there is no Google Fonts link anywhere). Archivo carries a WIDTH axis
   as well as weight — the display headings run wide, which is the typographic
   signature of this site and the reason the family was picked. */
@font-face {
  font-family: 'Archivo';
  src: url('/assets/fonts/archivo-var.woff2') format('woff2');
  font-weight: 400 800;
  font-stretch: 62% 125%;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url('/assets/fonts/jetbrains-mono-var.woff2') format('woff2');
  font-weight: 400 600;
  font-style: normal;
  font-display: swap;
}

/* ── Tokens ───────────────────────────────────────────────────────────────── */
:root {
  color-scheme: dark;

  /* Ground and surfaces — four steps, all neutral. The spectrum never fills a
     surface; it only ever draws a line, a ring, a glow or a button. */
  --ink-0: #06080C;   /* page ground */
  --ink-1: #0B0F16;   /* raised band */
  --ink-2: #121826;   /* card / input surface */
  --ink-3: #1A2233;   /* hover surface */

  /* Three jobs, three tokens, and the difference is a WCAG one. --line is
     DECORATION (lane ticks, rules, table hairlines, section edges) where 1.4.11
     does not apply, so it stays quiet. --line-2 is the boundary of a real
     component — an input, a button, a tag — and measures 3.54:1 on --ink-0,
     3.39:1 on --ink-1 and 3.13:1 on --ink-2, all clear of the 3:1 floor.
     --line-card sits between them: a resting card edge, visible but not a grid
     of boxes; an interactive card takes --line-2 instead (see .card--tool). */
  --line:      #1E2634;  /* decorative hairlines */
  --line-tick: #2B3547;  /* lane ticks — texture, never a boundary */
  --line-card: #3F4B66;  /* resting card / note edge */
  --line-2:    #5A6784;  /* the edge of a component you must be able to find */
  --line-lit:  #7E8CAA;  /* hover / active borders, and link underlines */

  /* Text. Measured against --ink-0: --text ≈ 17:1, --text-dim ≈ 7.6:1, both
     clear of 4.5:1. --text-faint measures ≈ 4.2:1, so it is reserved for
     DECORATION and large type — never for body copy or small labels. */
  --text:       #E9EFF7;
  --text-dim:   #93A1B4;
  --text-faint: #66748A;

  /* The six spectrum stops, in order. */
  --s1: #FF4D6D;
  --s2: #FF9F45;
  --s3: #FFE45E;
  --s4: #4CE0A0;
  --s5: #3AC7FF;
  --s6: #8B7BFF;

  --spectrum: linear-gradient(90deg, var(--s1), var(--s2), var(--s3), var(--s4), var(--s5), var(--s6));
  /* The same six stops TWICE. Anything that slides a window across the
     spectrum uses this at background-size: 200% 100%, so the 100%-wide window
     always frames one complete red-to-violet pass no matter where the sweep
     has got to. A single pass at 200% frames only half of it, which is how the
     Rainbow Road reference ends up on screen as a sunset-to-teal smear. */
  --spectrum-2x: linear-gradient(90deg,
      var(--s1), var(--s2), var(--s3), var(--s4), var(--s5), var(--s6),
      var(--s1), var(--s2), var(--s3), var(--s4), var(--s5), var(--s6));
  /* The same six at 18% for glows. Written out with rgba() rather than
     color-mix(): Safari's support for color-mix has been uneven and this
     gradient is load-bearing on every hover state. */
  --spectrum-soft: linear-gradient(90deg,
      rgba(255, 77, 109, .18), rgba(255, 159, 69, .18), rgba(255, 228, 94, .18),
      rgba(76, 224, 160, .18), rgba(58, 199, 255, .18), rgba(139, 123, 255, .18));

  /* Per-card hue, reassigned in sequence inside .cards and .stack (see "Cards").
   Two tokens, both consumed at REST: --hue draws the tick above a card title,
   the tag dot and the tool-card arrow; --hue-soft is the bloom. A third,
   --hue-line, used to be dealt out beside them and read by nothing but a hover
   state that only .card--tool ever had — eighteen declarations painting nothing.
   Removed; a hover border takes the full var(--hue), which also keeps the
   border above 3:1 instead of dropping it to a .45 tint. */
  --hue: var(--s5);
  --hue-soft: rgba(58, 199, 255, .18);

  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 18px;
  --r-pill: 999px;

  /* --step is the rhythm unit and doubles as the tick spacing on every lane:
     one tick per step, the way a timeline ruler marks the grid. */
  --step: 20px;
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;
  --sp-8: 64px;
  --sp-9: 96px;

  --ease: cubic-bezier(.2, .7, .2, 1);

  --font-sans: 'Archivo', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  --head-h: 60px;   /* tall state; the compact state is ~52px */
  --wrap: 1120px;
  --gutter: 20px;
  /* ONE MEASURE FOR EVERY LINE OF TEXT ON THE SITE. 624px is 68 characters of
     Archivo at 16px — the reading measure the body copy already used — and it is
     written in PIXELS, not in ch, on purpose: `ch` resolves against the
     ELEMENT's own font size, so a shared `68ch` token would give a 60px h1 a
     2300px cap and a 13px footnote a 500px one. There were two verticals on
     every page, 97px apart, because .wrap--narrow capped at 720px while p, ul,
     .faq and .steps capped at 68ch. Everything that holds prose now points here:
     p, ul/ol, .lede, .faq, .steps, .note, h1, .wrap--narrow > * and the footer
     small print. One right edge per page. */
  --measure: 624px;
}

/* ── Base ─────────────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Anchor targets must clear the sticky header in its tall state. */
  scroll-padding-top: calc(var(--head-h) + 16px);
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background-color: var(--ink-0);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 16px;
  font-weight: 400;
  font-stretch: 100%;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

img, svg, video { max-width: 100%; height: auto; }

a {
  color: var(--text);
  text-decoration-color: var(--line-lit);
  text-decoration-thickness: 1px;
  text-underline-offset: .18em;
  transition: color .18s var(--ease), text-decoration-color .18s var(--ease);
}
a:hover { color: #fff; text-decoration-color: var(--s5); }

strong, b { font-weight: 650; }
/* ONE DIVIDER, ONE MEANING. There used to be two — the ticked .lane between
   sections and a plain `hr.rule` hairline, both separating the same kind of gap,
   sometimes on the same page. .rule is gone; every separator on the site is a
   .lane. This base rule only covers a bare <hr> nobody styled. */
hr { border: 0; border-top: 1px solid var(--line); margin: var(--sp-6) 0; }

/* One focus treatment for the whole site: a spectrum-tinted ring with a soft
   bloom, offset so it never sits on the element's own border. Outlines are
   never removed anywhere in this file without this taking their place. */
:focus-visible {
  outline: 2px solid var(--s5);
  outline-offset: 2px;
  border-radius: var(--r-sm);
  box-shadow: 0 0 0 5px rgba(58, 199, 255, .16);
}

::selection { background: rgba(58, 199, 255, .28); color: #fff; }

/* ── Layout ───────────────────────────────────────────────────────────────── */
/* THE SPACING SCALE. Five steps do the whole site, and a value outside them is
   a bug unless there is a note beside it saying what it measures:

     --sp-2   8px   a label to the thing it labels (.eyebrow to its heading)
     --sp-3  12px   inside one component (a card title to its tag, a step
                    number to its line, a label to its value)
     --sp-4  16px   one paragraph to the next; a heading to the block under it
     --sp-5  24px   one component to the next inside a section
     --sp-7/8       one section to the next

   Everything below is measured on that, including the footer. */
.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
/* ONE LEFT VERTICAL FOR THE WHOLE SITE. .wrap--narrow used to be a 720px box
   centred in the viewport, so /account, /privacy, /terms and every narrow
   section of the hub started ~200px right of the wordmark above them, the
   section headings beside them and the entire footer below them — all on the
   same page, at the same width. The box now runs the full measure and the
   CONTENT is capped instead: a narrow column starts exactly where a wide one
   does, and the short edge is on the right where nobody is reading. */
.wrap--narrow > * { max-width: var(--measure); }

.section { padding-block: var(--sp-7); }
.section--tight { padding-block: var(--sp-5); }
@media (min-width: 720px) {
  .section { padding-block: var(--sp-8); }
  .section--tight { padding-block: var(--sp-6); }
}

.stack > * { margin-block: 0; }
.stack > * + * { margin-top: var(--sp-5); }

/* A HEADING TO THE BLOCK UNDER IT. Prose after a heading takes the heading's own
   margin and stops there; a component — a grid of cards, a step list, a table of
   facts, a callout — needs the next step up, or a 34px h2 sits 12px off the top
   of one and reads as its caption. This is the one place a heading's spacing is
   decided by what FOLLOWS it rather than by its level.
   ⚠ These are the WHOLE gap, not an addition to it: adjacent vertical margins
   collapse, so what lands on the page is max(heading's bottom, this), never the
   sum. Writing --sp-4 here bought exactly nothing over the h2 that already had
   --sp-4 under it — measured at 16px, unchanged. */
:is(h1, h2, .h1, .h2) + :is(.cards, .card, .steps, .stack, .faq, .faq-group, .kv, .note, .form, .threads, .msg-thread, .lane, .alert, .form-actions) {
  margin-top: var(--sp-6);
}
:is(h3, h4, .h3, .h4) + :is(.cards, .card, .steps, .stack, .faq, .faq-group, .kv, .note, .form, .threads, .msg-thread, .lane, .alert, .form-actions) {
  margin-top: var(--sp-5);
}
/* ⚠ .stack IS IN THE LIST ABOVE AND IS THE ONE MEMBER THAT IS NOT A COMPONENT.
   It is whatever it happens to hold: a grid of cards on the hub, four sentences
   on /privacy. Left in the list unconditionally it gave the legal pages a
   component-sized 32px gap under every heading while a bare <p> section on the
   same page took 16 — /terms measured 32, 32, 32, 32, 16, 16, 32 for the same
   relationship. So a .stack that OPENS ON PROSE drops back to the prose gap,
   which is the heading's own margin and what a bare paragraph already gets.
   A .stack that opens on a card grid or a step list keeps the 32.
   These are the WHOLE gap, not an addition: adjacent margins collapse. */
:is(h1, h2, .h1, .h2) + .stack:has(> :first-child:is(p, ul, ol)) { margin-top: var(--sp-4); }
:is(h3, h4, .h3, .h4) + .stack:has(> :first-child:is(p, ul, ol)) { margin-top: var(--sp-4); }

/* A .note carries the most important caveat on the page and usually lands under
   a step list or a card grid, where a component-sized gap left it looking
   attached to the last item rather than following it. */
.stack > .note,
.steps + .note,
.cards + .note,
.kv + .note { margin-top: var(--sp-6); }
.note + .note { margin-top: var(--sp-4); }

/* CHANNEL LANE (motif 2). A baseline hairline with a tick every --step, the
   same 20-unit grid the sequencer runs on. Purely decorative, so it is drawn
   with gradients and carries no markup or image. */
/* ⚠ content-box, not the default padding-box. A lane between two sections is a
   direct child of <main>, so the safety-net rule below hands it the .wrap
   max-width AND the gutter. A percentage background is measured from the
   padding box, which INCLUDES that gutter — so the hairline and its ticks
   started 20px left of the wordmark, every heading and the whole footer, and
   ended flush with them: a third left vertical on a page that has two. */
.lane {
  height: 10px;
  margin-block: var(--sp-6);
  background-repeat: no-repeat;
  background-origin: content-box;
  background-image:
    linear-gradient(var(--line), var(--line)),
    repeating-linear-gradient(90deg, var(--line-tick) 0 1px, transparent 1px var(--step));
  background-size: 100% 1px, 100% 7px;
  background-position: left bottom, left bottom;
}

.site-main { flex: 1 0 auto; }

/* Safety net: a page that drops content straight into <main> without its own
   .wrap still lines up with every wrapped section instead of running edge to
   edge. Pages should still use `.section > .wrap`; this only stops a mistake
   from looking like a broken site. */
.site-main > h1, .site-main > h2, .site-main > h3, .site-main > p,
.site-main > ul, .site-main > ol, .site-main > dl, .site-main > .note,
.site-main > .cards, .site-main > .card, .site-main > .steps,
.site-main > .faq, .site-main > .form, .site-main > .kv,
.site-main > .lane, .site-main > .alert,
.site-main > .eyebrow, .site-main > .lede {
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
/* A lane dropped BETWEEN two sections is the divider, and the sections' own
   padding is the space around it — it must not add a third gap of its own.
   64 + 32 + 32 + 64 put nearly two hundred pixels between a heading and the
   last line above it, and left the hairline nowhere near the middle of it. */
.site-main > .lane { margin-block: 0; }
/* ⚠ A DIVIDER MUST SIT IN THE MIDDLE OF THE GAP IT DIVIDES. .section pads 64 and
   .section--tight pads 32, so a lane between the two was 32px from one block and
   64px from the other — measurably glued to the tight side, and it read as
   belonging to it. The make-up margin goes on whichever side is short. */
.site-main > .section--tight + .lane { margin-top: var(--sp-5); }
.site-main > .lane:has(+ .section--tight) { margin-bottom: var(--sp-5); }
@media (min-width: 720px) {
  .site-main > .section--tight + .lane { margin-top: var(--sp-6); }
  .site-main > .lane:has(+ .section--tight) { margin-bottom: var(--sp-6); }
}

/* ── Type ─────────────────────────────────────────────────────────────────── */
/* Display headings run WIDE (the wdth axis) and heavy. Wide display against
   normal-width body is the signature; it echoes the car's light bar. */
/* Two steps, by size rather than by level: the two display sizes open a section
   and need the section's own step under them; h3 and h4 open a paragraph and
   need the paragraph's. */
h1, .h1, h2, .h2, h3, .h3, h4, .h4 {
  font-family: var(--font-sans);
  color: var(--text);
  margin: 0 0 var(--sp-3);
  line-height: 1.08;
  text-wrap: balance;
}
h1, .h1, h2, .h2 { margin-bottom: var(--sp-4); }
h1, .h1 {
  font-size: clamp(34px, 7vw, 60px);
  font-weight: 800;
  font-stretch: 118%;
  letter-spacing: -.02em;
  /* The headline stops where its own lede stops. Uncapped, a sub-page h1 ran the
     full 1080px wrap while the sentence under it stopped at the measure, so
     /light-show alone had four different right edges inside one hero. */
  max-width: var(--measure);
}
h2, .h2 {
  font-size: clamp(24px, 4vw, 34px);
  font-weight: 750;
  font-stretch: 112%;
  letter-spacing: -.02em;
  line-height: 1.14;
}
h3, .h3 {
  font-size: 20px;
  font-weight: 700;
  font-stretch: 106%;
  letter-spacing: -.01em;
  line-height: 1.25;
}
/* h4 exists so a task group (h3 "Light show") can own its steps instead of the
   steps being siblings of it in a screen reader's heading list. It reads a
   notch quieter than h3 and otherwise inherits the same treatment. */
h4, .h4 {
  font-size: 17px;
  font-weight: 700;
  font-stretch: 104%;
  letter-spacing: -.005em;
  line-height: 1.3;
}

p { margin: 0 0 var(--sp-4); max-width: var(--measure); }
p:last-child { margin-bottom: 0; }

.lede {
  font-size: clamp(17px, 2.2vw, 20px);
  line-height: 1.55;
  color: var(--text);
  max-width: var(--measure);
}
.muted { color: var(--text-dim); }

/* ⚠ AN EYEBROW INSIDE A .stack KEEPS ITS OWN 8px GAP. .stack hands every child
   after the first a 24px top margin, which beat the eyebrow's 8px bottom one —
   so the label sat 8px above its heading on nine pages and 24px above it on
   /account and /contact, and /account switched between the two depending on
   whether anyone was signed in. */
.stack > .eyebrow + * { margin-top: var(--sp-2); }

.eyebrow {
  display: block;
  font-size: 12px;
  font-weight: 650;
  font-stretch: 108%;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-dim);
  /* An eyebrow belongs TO the heading under it, so it sits at the label step
     rather than the paragraph one. At 12px against a 34px h2 the wider gap read
     as a line of its own floating above the section. */
  margin: 0 0 var(--sp-2);
}

/* DATA IN MONO (motif 4). Tabular figures so a column of timecodes or byte
   counts lines up down the page. */
.mono, code, kbd, samp, pre {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}
.mono { font-size: .92em; letter-spacing: -.01em; }
code {
  font-size: .88em;
  padding: .12em .38em;
  border-radius: var(--r-sm);
  background: var(--ink-2);
  border: 1px solid var(--line);
  color: var(--text);
}
pre {
  overflow-x: auto;
  padding: var(--sp-4);
  border-radius: var(--r-md);
  background: var(--ink-1);
  border: 1px solid var(--line);
}
pre code { background: none; border: 0; padding: 0; }

/* Used ONCE per page at most — a gradient headline everywhere is exactly the
   cliché this design is avoiding.
   ⚠ display MATTERS. background-clip: text clips the gradient to the glyphs but
   the gradient itself is painted across the ELEMENT's box, so on a block-level
   <p> in a 680px column the three digits of "404" occupied the first ~11% of
   the box and the type rendered flat coral — red to barely orange, no yellow,
   no green, no violet, and a different colour at every viewport width.
   inline-block sizes the box to the glyphs, so one whole red-to-violet pass
   lands across whatever the text actually is. */
.spectrum-text {
  display: inline-block;
  background-image: var(--spectrum);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}

ul, ol { padding-left: 1.15em; max-width: var(--measure); }
li + li { margin-top: var(--sp-2); }

/* ── Header ───────────────────────────────────────────────────────────────── */
/* MOSTLY EMPTY SPACE. A wordmark on the left, four small labels on the right, a
   hairline underneath, and the spectrum progress rail riding that hairline.
   Nothing else.

   It used to be a mixer channel strip: a tick-lane texture painted behind the
   whole bar, five items each carrying a two-digit mono index stacked above its
   label, a spectrum hue dealt per channel, a filled chip on hover and the
   indices folding away on scroll. That is a great deal of furniture to put at
   the top of every page, and it was the first thing anybody saw. Removed, not
   restyled — the texture, the indices, the hue cycle, the hover fill and the
   Home item are all gone, the labels dropped a size, and the current page is
   marked by a 1px spectrum hairline instead of a 2px bar.

   It still SHRINKS on scroll: .is-compact reduces the padding and the wordmark
   only, so nothing reflows. */
.site-head {
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(6, 8, 12, .86);
  -webkit-backdrop-filter: saturate(140%) blur(12px);
  backdrop-filter: saturate(140%) blur(12px);
  border-bottom: 1px solid var(--line);
}

.site-head__inner {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding-block: 8px;
  transition: padding-block .22s var(--ease);
}
.site-head.is-compact .site-head__inner { padding-block: 4px; }

/* THE SPECTRUM RAIL (motif 1). --p is a 0..1 fraction set by ui.js. The
   gradient is sized to the viewport rather than to the element, so revealing it
   left to right slides a window over a fixed spectrum instead of squashing it
   into a smaller and smaller box. */
.site-head__rail {
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 2px;
  width: calc(var(--p, 0) * 100%);
  /* The gradient is sized to the VIEWPORT, not to the rail, so the colour under
     any point on screen stays put as the bar grows — the rail reveals the
     spectrum rather than squashing it. It also slides on its own, so the light
     is alive before anyone scrolls. */
  background-image: var(--spectrum-2x);
  background-size: 200vw 100%;
  background-repeat: repeat-x;
  background-position-x: 0;
  pointer-events: none;
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  /* The primary "back to home" control on a phone, so it carries the 44px
     floor. Only the type and the mark shrink in the compact state; the tap
     target does not. */
  min-height: 44px;
  font-size: 20px;
  font-weight: 800;
  font-stretch: 118%;
  letter-spacing: -.03em;
  color: var(--text);
  text-decoration: none;
  transition: font-size .22s var(--ease);
}
.site-head.is-compact .wordmark { font-size: 17px; }
.wordmark:hover { color: #fff; }

/* The mark IS Derrick's light-bar artwork, not an abstraction of it: the PNG is
   an alpha mask (white where the lights are) and the spectrum shows through it,
   the same technique the hero uses at full size. aspect-ratio keeps the art's
   own 1101:192 proportion at every size. */
.wordmark__mark {
  display: block;
  width: 34px;
  aspect-ratio: 1101 / 192;
  flex: none;
  background-image: var(--spectrum);
  -webkit-mask-image: url('/images/mark-mask.png');
  mask-image: url('/images/mark-mask.png');
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  transition: width .22s var(--ease);
}
.site-head.is-compact .wordmark__mark { width: 30px; }

/* Nav. WITHOUT JavaScript the full nav is always visible and simply wraps onto
   a second line below 720px, so every page stays reachable.
   ⚠ THE COLLAPSE IS GATED ON data-nav="ready", which ui.js writes only AFTER it
   has actually bound the toggle — never from the head, and never on load alone.
   A flag set before the binding is confirmed is a phone-sized trap: if ui.js
   404s (this repo's FTPS sync is documented to skip a committed file until it is
   renamed), gets the wrong MIME type or throws on parse, the nav is hidden, the
   Menu button does nothing, and every page but the current one is unreachable.
   Gating on the binding costs one frame of reflow and cannot strand anyone. */
.site-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 var(--sp-2);
}

/* ONE WORD PER DESTINATION. Archivo at 14px, run slightly narrow so the row
   reads as labelling rather than as more headline, dim until it is the page you
   are on or the pointer is over it. No index, no chip, no fill: the only thing
   that changes on hover is the text colour. */
.nav-link {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--sp-2);
  font-size: 14px;
  font-weight: 550;
  font-stretch: 98%;
  letter-spacing: .004em;
  line-height: 1.2;
  white-space: nowrap;
  color: var(--text-dim);
  text-decoration: none;
  transition: color .18s var(--ease);
}
.nav-link:hover { color: var(--text); }
.nav-link.is-active { color: var(--text); }

/* THE MARK. 1px of spectrum the width of the label, sitting close under it —
   the quietest form the rail takes anywhere on the site. It was a 2px bar
   across a padded chip, which at 14px type is a rule with a word balanced on
   top of it. */
.nav-link.is-active::after {
  content: '';
  position: absolute;
  left: var(--sp-2);
  right: var(--sp-2);
  bottom: 9px;
  height: 1px;
  background-image: var(--spectrum);
}

/* The Menu button, with the chrome taken off: no border, no fill, just the bars
   and a mono label. They still fold into a cross on [aria-expanded='true'] —
   CSS off an attribute the existing script already sets, so the state costs no
   new JavaScript. */
.nav-toggle {
  display: none;
  align-items: center;
  gap: 9px;
  min-height: 44px;
  padding: 0 var(--sp-2);
  font: inherit;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-dim);
  background: transparent;
  border: 0;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: color .18s var(--ease);
}
.nav-toggle:hover { color: var(--text); }
/* Three 2px rules on a 5px pitch — the same lane rhythm, at button scale. */
.nav-toggle__bars {
  position: relative;
  display: block;
  width: 16px;
  height: 2px;
  flex: none;
  background: currentColor;
  transition: background-color .18s var(--ease);
}
.nav-toggle__bars::before,
.nav-toggle__bars::after {
  content: '';
  position: absolute;
  left: 0;
  width: 16px;
  height: 2px;
  background: currentColor;
  transition: transform .2s var(--ease);
}
.nav-toggle__bars::before { top: -5px; }
.nav-toggle__bars::after  { top: 5px; }
.nav-toggle[aria-expanded='true'] .nav-toggle__bars { background: transparent; }
.nav-toggle[aria-expanded='true'] .nav-toggle__bars::before { transform: translateY(5px) rotate(45deg); }
.nav-toggle[aria-expanded='true'] .nav-toggle__bars::after  { transform: translateY(-5px) rotate(-45deg); }

@media (max-width: 719.98px) {
  :root[data-nav='ready'] .nav-toggle { display: inline-flex; }
  :root[data-nav='ready'] .site-head__inner { flex-wrap: wrap; }
  /* The same four labels, stood on end. A hairline between rows and nothing
     else: no tick lane, no index column, no tinted active row. */
  :root[data-nav='ready'] .site-nav {
    display: none;
    flex-basis: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    margin-top: var(--sp-2);
    padding-bottom: var(--sp-3);
  }
  :root[data-nav='ready'] .site-nav.is-open { display: flex; }
  :root[data-nav='ready'] .nav-link {
    /* The base rule is a centred inline row; a column of centred links is
       exactly the generic phone menu this is not. */
    justify-content: flex-start;
    min-height: 50px;
    padding: 0;
    font-size: 16px;
    border-top: 1px solid var(--line);
  }
  /* The same spectrum mark the desktop row carries, cut to a short dash at the
     start of the row so it does not run the width of the panel. */
  :root[data-nav='ready'] .nav-link.is-active::after {
    left: 0;
    right: auto;
    width: 26px;
    bottom: 11px;
  }
  /* No JS, or JS that never bound: the row wraps under the wordmark and stays
     fully usable. This is also the state a broken ui.js falls back to. */
  :root:not([data-nav='ready']) .site-head__inner { flex-wrap: wrap; }
  :root:not([data-nav='ready']) .site-nav { flex-basis: 100%; }
}

/* ── Hero ─────────────────────────────────────────────────────────────────── */
/* A DARK PAGE WITH A LIT STRIP ON IT, and nothing else. There used to be a
   vertical tick texture painted across the whole band as well, fading downward
   behind the type; between that and the bloom below, the top of the site was a
   haze rather than a light. The ruler still runs through the page — that is
   what .lane is — but it does not sit behind the headline. */
.hero {
  position: relative;
  overflow: hidden;
  padding-block: var(--sp-7) var(--sp-6);
  border-bottom: 1px solid var(--line);
}

.hero__copy { max-width: 54ch; }
.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: var(--sp-5);
}
/* Deliberately normal inline flow, not a flex row: a page uses this for either
   a short row of facts or one plain sentence of status, and flex would turn
   every <span class="mono"> inside a sentence into its own flex item with a gap
   on each side, which shreds the spacing of the prose around it. */
.hero__meta {
  margin-top: var(--sp-5);
  max-width: 64ch;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-dim);
}

/* Three discrete facts, not one run-on sentence. The separator is drawn between
   the spans rather than making this a flex container: an earlier flex version
   turned every inline <span class="mono"> inside a status line into its own flex
   item and shredded the spacing, so inline flow is kept and a dot does the work. */
/* Each fact is one unbreakable unit. Without this the third one wraps mid-range
   ("Model Y 2020-" / "2024 first"), and in tabular mono a split range reads as a
   rendering fault rather than a line break. The container still wraps BETWEEN
   the spans, which is where the break belongs. */
.hero__meta > span { white-space: nowrap; }
.hero__meta > span + span::before {
  content: "";
  display: inline-block;
  width: 3px;
  height: 3px;
  margin: 0 10px 3px;
  border-radius: 50%;
  background: var(--line-lit);
}

/* THE SIGNATURE. /images/lights-mask.png is Derrick's own drawing of a front
   light bar, converted to an alpha mask: the spectrum shows THROUGH the lights.
   ONE LAYER, and the mask is on it.

   ⚠ THERE WAS A SECOND LAYER AND IT IS NOT COMING BACK. ::before used to paint
   the same gradient UNMASKED under filter: blur(26px) at .35 opacity, squashed
   vertically — a bloom, on the reasoning that a blur applied to the masked
   layer would be clipped back to the mask. What it actually did was smear a
   full-width band of colour across the hero: the spectrum stopped being a lit
   bar and became a coloured fog with a drawing floating in it, which is the
   opposite of the app icon and the favicon, where the colour is confined to the
   light strip itself. The mask was regenerated with a hard alpha ramp for the
   same reason. The light lives in the bars. */
.hero__lights {
  position: relative;
  width: 100%;
  max-width: 820px;
  aspect-ratio: 1800 / 503;
  /* Centred, so it reads correctly whether a page nests it in the hero grid or
     drops it straight into a full-bleed .hero. */
  margin: var(--sp-6) auto 0;
}
.hero__lights::after {
  content: '';
  position: absolute;
  inset: 0;
  /* --spectrum-2x, not --spectrum: at 200% background-size the visible window
     is exactly half the image, so a doubled gradient always shows one whole
     red-through-violet pass while the sweep still slides. */
  background-image: var(--spectrum-2x);
  background-size: 200% 100%;
  background-position: 0% 50%;
  /* ⚠ IT DOES NOT MOVE, AND THAT IS THE WHOLE POINT.
     This ran a 22s sweep, so at every moment except the loop's origin the six
     stops were rotated: the bar came out green → cyan → blue → pink → red →
     orange left to right, with a visible hue break where each wing meets the
     centre. images/icon-512.png and favicon-64.png run ONE clean red-to-violet
     pass, continuous through the wings. Pinned at 0% the art is pixel for pixel
     the app icon, which is what the brief asked for. */
  -webkit-mask-image: url('/images/lights-mask.png');
  mask-image: url('/images/lights-mask.png');
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}
/* ⚠ NOTHING ON THIS SITE LOOPS ANY MORE, and the three @keyframes that drove
   the spectrum surfaces are gone with the declarations that used them. Six
   infinite animations ran at all times — the header rail, the hero art, the
   primary button's ring, the checked checkbox, the footer hairline and the
   back-to-top button — on a site whose whole brief is lightweight, which spent
   the entire motion budget on decoration. The spectrum is now a fixed material:
   every one of those surfaces is pinned to the frame the reduced-motion block
   already declared as the correct one, so what everybody sees is what a person
   with motion turned off was already getting. */

@media (min-width: 1040px) {
  .hero { padding-block: var(--sp-8) var(--sp-7); }
  .hero__inner { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, .9fr); gap: var(--sp-7); align-items: center; }
  .hero__lights { margin-top: 0; }
}

/* ── Buttons ──────────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 44px;
  padding: 10px 18px;
  font: inherit;
  font-size: 15px;
  font-weight: 650;
  font-stretch: 104%;
  letter-spacing: -.01em;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  background: var(--ink-2);
  color: var(--text);
  cursor: pointer;
  transition: background-color .18s var(--ease), border-color .18s var(--ease), color .18s var(--ease), box-shadow .18s var(--ease);
}
.btn:hover {
  background: var(--ink-3);
  border-color: var(--line-lit);
  color: #fff;
}

/* The one place the spectrum fills a shape. Dark ink on it clears 6:1 against
   even the darkest stop (--s6). */
.btn--primary {
  /* A rainbow FILL with dark text is the version of this button that cannot be
     read: the text sits on six different backgrounds at once and loses against
     the red and violet ends. So the spectrum moves to the EDGE — two background
     layers, the solid interior clipped to the padding box and the gradient to
     the border box — and the label goes back to plain light text on the page
     ground, which is legible against every stop. The gradient is there at rest
     rather than only on hover; it is a light, and lights do not wait to be
     pointed at. It does not travel — see the note under the hero. */
  color: var(--text);
  border: 2px solid transparent;
  background-color: transparent;
  background-image: linear-gradient(var(--ink-1), var(--ink-1)), var(--spectrum-2x);
  background-size: auto, 200% 100%;
  background-position: 0 0, 0% 50%;
  background-origin: padding-box, border-box;
  background-clip: padding-box, border-box;
  background-repeat: no-repeat, no-repeat;
  font-weight: 700;
  transition: box-shadow .18s var(--ease), color .18s var(--ease);
}
.btn--primary:hover {
  color: #fff;
  box-shadow: 0 0 26px rgba(58, 199, 255, .20);
}
.btn--primary:active { box-shadow: none; }

.btn--ghost {
  background: transparent;
  border-color: var(--line-2);
  color: var(--text);
}
.btn--ghost:hover { background: var(--ink-1); border-color: var(--line-lit); }

.btn--sm { min-height: 38px; padding: 6px 14px; font-size: 14px; }
.btn--block { display: flex; width: 100%; }

.btn[disabled], .btn[aria-disabled='true'] {
  opacity: .45;
  cursor: not-allowed;
  pointer-events: none;
  box-shadow: none;
}

/* ── Cards ────────────────────────────────────────────────────────────────── */
/* ILLUMINATION, NOT ELEVATION (motif 3). No shadows at rest, ever. On hover or
   focus-within the border takes the card's hue and a bloom appears behind it.
   Cards inside a .cards grid are dealt hues in spectrum order, so a row of
   three or four reads as part of one spectrum. */
/* The gap IS the boundary now that .card--tool and .card--quiet have no border,
   so it is a step wider than it was when every cell was a box. */
.cards { display: grid; gap: var(--sp-6); }
@media (min-width: 720px) {
  .cards { gap: var(--sp-6) var(--sp-7); }
  .cards--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .cards--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1040px) {
  .cards--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* The hue is dealt to .stack children as well as .cards children: the signed-in
   account page and several sections stack their cards in one column rather than
   a grid, and a column that comes out uniformly cyan is the hue system quietly
   doing nothing. */
.cards > *:nth-child(6n+1), .stack > *:nth-child(6n+1) { --hue: var(--s1); --hue-soft: rgba(255, 77, 109, .18); }
.cards > *:nth-child(6n+2), .stack > *:nth-child(6n+2) { --hue: var(--s5); --hue-soft: rgba(58, 199, 255, .18); }
.cards > *:nth-child(6n+3), .stack > *:nth-child(6n+3) { --hue: var(--s3); --hue-soft: rgba(255, 228, 94, .18); }
.cards > *:nth-child(6n+4), .stack > *:nth-child(6n+4) { --hue: var(--s4); --hue-soft: rgba(76, 224, 160, .18); }
.cards > *:nth-child(6n+5), .stack > *:nth-child(6n+5) { --hue: var(--s6); --hue-soft: rgba(139, 123, 255, .18); }
.cards > *:nth-child(6n+6), .stack > *:nth-child(6n+6) { --hue: var(--s2); --hue-soft: rgba(255, 159, 69, .18); }

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--sp-5);
  /* --line measured 1.26:1 against --ink-1 — a border nobody can see is not a
     card, it is a paragraph with padding. --line-card is the resting edge;
     anything you can actually click takes --line-2 (see .card--tool). */
  border: 1px solid var(--line-card);
  border-radius: var(--r-lg);
  background: var(--ink-1);
  color: inherit;
  text-decoration: none;
  transition: border-color .22s var(--ease), background-color .22s var(--ease), box-shadow .22s var(--ease);
}
/* The card's hue, spent at REST rather than only on hover. A 28px segment of
   the spectrum above the title — the same line the header mark and the footer
   hairline draw, at card scale. A set of cards then reads as one spectrum
   standing still, which is what the hue cycle above is for; without this it
   paints nothing until someone hovers, and touch users never do.
   Deliberately a top segment, not a side stripe.

   A FLEX COLUMN WITH A GAP, so the tick and the title are spaced by the
   container rather than by margins the title does not carry. It held a status
   pill under the title as well until those became dim text on the title line
   itself; the gap stays because it is what puts the tick 12px off the heading. */
.card__head {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-3);
  margin-bottom: var(--sp-4);
}
.card__head::before {
  content: '';
  display: block;
  width: 28px;
  height: 2px;
  border-radius: 2px;
  background: var(--hue);
}
.card__title {
  margin: 0;
  font-size: 20px;
  font-weight: 700;
  font-stretch: 108%;
  letter-spacing: -.01em;
  line-height: 1.25;
}
.card__body { flex: 1 1 auto; color: var(--text-dim); }
.card__body p { color: var(--text-dim); }
.card__foot {
  margin-top: var(--sp-4);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
}

/* THE TWO PRODUCT LINKS — an AREA OF THE PAGE, not a container.
   They used to be the heaviest things on the site: a full border at component
   contrast, an 18px radius, a raised ground, 32px of padding, a hue tick, a
   title, a pill, a paragraph, a hairline rule and a link under it. Nine pieces
   of furniture around two sentences, side by side, immediately under the hero.
   What is left is the hue tick, the words, and the arrow: no border, no fill and
   no divider at rest. The grid gap and the tick are the separation.

   The lit state does all the work instead — a tinted ground and a quiet bloom in
   the card's own hue fade UP on hover and focus, with no transform, no lift and
   no shadow (motif 3). Touch users, who never see it, are not missing a
   boundary: the arrow and the tick say the block is a link at rest. */
.card--tool {
  padding: 0;
  overflow: visible;
  border: 0;
  border-radius: var(--r-lg);
  background: transparent;
}
/* The lit state, drawn OUTSIDE the text box. The block itself has no padding, so
   its words start on the same left vertical as the heading above it; the plate
   that lights up is a pseudo-element inset 16px in every direction, which is the
   padding the card no longer carries, arriving only when it is wanted. 16px also
   keeps it inside the wrap's 20px gutter on a phone, so nothing bleeds off the
   page edge. */
.card--tool::before {
  content: '';
  position: absolute;
  inset: calc(var(--sp-4) * -1);
  border-radius: var(--r-lg);
  background-color: var(--ink-1);
  background-image: radial-gradient(70% 90% at 50% 0%, var(--hue-soft), transparent 70%);
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s var(--ease);
}
.card--tool:hover::before, .card--tool:focus-within::before { opacity: 1; }
.card--tool:hover .card__title { color: #fff; }

/* THE ARROW, and it is the whole affordance now. With the border gone there is
   nothing else at rest saying this block is a link, and a touch user never sees
   the lit state. It rides the anchor's own ::after rather than the title's,
   which keeps it out of the accessible name entirely — the card is named by
   aria-labelledby pointing at the heading, and generated content on the heading
   would have been read out as part of it.
   position: relative is load-bearing: ::before is absolutely positioned, and a
   positioned pseudo paints over a static one whatever the tree order. */
.card--tool::after {
  content: '\2192';
  position: relative;
  align-self: flex-start;
  margin-top: var(--sp-4);
  font-size: 19px;
  line-height: 1;
  color: var(--hue);
  transition: transform .22s var(--ease);
}
.card--tool:hover::after, .card--tool:focus-within::after { transform: translateX(4px); }
.card--tool .card__title { font-size: clamp(21px, 3vw, 26px); font-stretch: 112%; }
/* Everything inside sits above the bloom. */
.card--tool > * { position: relative; }
/* The call to action. The hairline rule over it is gone with the box: the space
   and the hue are enough, and a divider inside an unbordered area draws a line
   from nowhere to nowhere. */
.card--tool .card__foot {
  margin-top: var(--sp-4);
  padding-top: 0;
  border-top: 0;
  color: var(--hue);
  font-weight: 650;
}

/* ⚠ THE ONE RULE THAT DECIDES WHETHER A CARD IS A BOX.
   IN A GRID, A CARD IS AN AREA OF THE PAGE. ON ITS OWN, IT IS A PANEL.

   Every card on the site used to be a box: a border, an 18px radius, a raised
   ground and 32px of padding, whether it held a call to action, a table of
   published facts or two sentences of description. Put two or four of them
   under a heading and the section stops being content and becomes containers —
   which is what "these sections are bulky" was about.

   A grid already says these blocks belong together and where each one ends: the
   gap is the boundary and the hue tick is the opening, and reference material
   inside one — a .kv table — rules a hairline under every row of its own. So a
   card inside .cards drops the box.

   ⚠ THE SELECTOR IS GRID-SCOPED ON PURPOSE, and .card--quiet is deliberately
   NOT in it. Every quiet card on the site sits in a .cards grid but one: the
   "Your creations" panel on a signed-in /account, which stands in a stack
   between two ordinary bordered .card panels. Un-boxing that one would leave a
   single edgeless block in a column of boxes, which reads as a rendering fault
   rather than a decision. A card standing on its own in the flow of a page
   keeps its box, because nothing else there marks it out. */
.cards > .card {
  padding: 0;
  border: 0;
  background: transparent;
}

/* Reference material standing on its own: quieter than a panel, still a panel. */
.card--quiet {
  background: transparent;
  border-color: var(--line-card);
  padding: var(--sp-4);
}

@media (min-width: 720px) {
  .card { padding: var(--sp-6); }
  .card--quiet { padding: var(--sp-5); }
  /* Restated, not inherited: .card--tool carries the same specificity as .card,
     so the rule above would otherwise put the padding back on a block that is
     deliberately padding-free. .cards > .card is more specific and needs no
     restating — including for a .card--tool, which always lives in a grid. */
  .card--tool { padding: 0; }
}

/* ── Bits ─────────────────────────────────────────────────────────────────── */
/* ⚠ .tag IS GONE AND IS NOT COMING BACK. It was a bordered, filled, 999px pill
   with a 7px dot inside it — four pieces of chrome around one or two words —
   sitting 12px under a heading on eleven headings across the site. Once the
   product cards were un-boxed it was the ONLY bordered object left on them, and
   it measured closer to the heading it crowded than to the sentence it
   introduced. Every status it carried is now dim text on the title line
   ("Light Show — Ships second") or the first clause of the sentence already
   under it, which reads in a screen reader as part of the same thought instead
   of as a floating label. If a status ever needs marking again, mark it in the
   words. */
.badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: var(--r-sm);
  background: var(--ink-2);
  border: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* Label / value rows. Markup: <dl class="kv"><dt>Format</dt><dd>lightshow.fseq</dd></dl> */
.kv { margin: 0; display: grid; gap: 0; }
.kv > dt {
  font-size: 13px;
  color: var(--text-dim);
  padding-top: var(--sp-3);
}
.kv > dd {
  margin: 0 0 var(--sp-3);
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--line);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 14px;
  color: var(--text);
}
.kv > dd:last-child { border-bottom: 0; margin-bottom: 0; }
/* A note under a table of facts. The last row's own 12px of padding was the only
   thing between the two, so a caveat sat on the hairline it qualifies and read
   as another value in the column. */
.kv + p, .kv + .muted { margin-top: var(--sp-4); }
@media (min-width: 720px) {
  /* The gutter is padding INSIDE dt, never a grid column-gap: the border-bottom
     spans dt's whole column either way, so the hairline under label and value
     stays one continuous rule. A column-gap would cut it into two segments with
     a visible break. Without any gutter a long label ("Several shows on one
     drive") butts straight into its value.

     Items STRETCH rather than baseline-align: a label that wraps to two lines is
     taller than its value, and under baseline alignment each box drew its own
     bottom border at its own height, stepping the row's hairline. Stretching
     makes both fill the row so the rule stays level; both already carry the same
     10px top padding, so the first lines still line up.

     ⚠ 26%, NOT 34%. At the wider figure the visible gutter was 16px of fixed
     padding plus whatever a short label left over, so one row read at 20px and
     the row two below it at 133px — a table whose rows do not agree with each
     other. Narrow enough that the long labels wrap, the fixed padding dominates
     and every row's gutter is the same. */
  .kv { grid-template-columns: minmax(120px, 26%) 1fr; }
  .kv > dt { padding: var(--sp-3) var(--sp-4) var(--sp-3) 0; border-bottom: 1px solid var(--line); }
  .kv > dd { padding-top: var(--sp-3); margin-bottom: 0; }
  .kv > dt:last-of-type, .kv > dd:last-child { border-bottom: 0; }
}

/* Numbered steps, number in mono. Works with <ol class="steps"><li class="step">
   or plain divs — the counter is on the class, not the element. */
.steps { counter-reset: frunkshow-step; list-style: none; padding: 0; margin: 0; max-width: var(--measure); }
.step {
  counter-increment: frunkshow-step;
  position: relative;
  /* 30px, not 46. The number is a number now, not a chip, so it needs the width
     of two digits and a space rather than the width of a box. */
  padding-left: 30px;
  margin: 0 0 var(--sp-5);
}
.step:last-child { margin-bottom: 0; }
/* THE NUMBER, WITH THE BOX TAKEN OFF. It used to be a 32x26 bordered, filled
   chip, which is a boxed label for a sequence the three headings already read
   as — and the 46px indent it needed opened a second left vertical on the hub,
   the only indented text on a page whose every other left edge was identical. */
.step::before {
  content: counter(frunkshow-step, decimal-leading-zero);
  position: absolute;
  left: 0;
  top: 2px;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-dim);
}

/* FAQ */
.faq { border-top: 1px solid var(--line); max-width: var(--measure); }
.faq > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  min-height: 44px;
  padding: var(--sp-3) 0;
  font-weight: 650;
  font-stretch: 104%;
  border-bottom: 1px solid var(--line);
  transition: color .18s var(--ease);
}
.faq > summary::-webkit-details-marker { display: none; }
.faq > summary::after {
  content: '';
  flex: none;
  width: 9px;
  height: 9px;
  border-right: 1.5px solid var(--text-dim);
  border-bottom: 1.5px solid var(--text-dim);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .2s var(--ease);
}
.faq[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.faq > summary:hover { color: #fff; }
.faq > :not(summary) { padding: var(--sp-4) 0; border-bottom: 1px solid var(--line); color: var(--text-dim); }

/* AN ASIDE, NOT A BOX. It was 24px of padding inside a --line-card border on an
   --ink-1 ground, which the CSS's own comment conceded was doing nothing (1.04:1
   against the page) — a border earning its keep only because it was the only
   boundary. Measured at 1280 it was a 720px box in a 1080px wrap on five pages,
   and on the two tool pages it was the ONLY bordered object in an otherwise
   borderless hero, around three words. What is left is the spectrum tick the
   cards use, dim text, and the space around it. */
.note {
  max-width: var(--measure);
  color: var(--text-dim);
}
.note::before {
  content: '';
  display: block;
  width: 28px;
  height: 2px;
  margin-bottom: var(--sp-3);
  border-radius: 2px;
  background: var(--hue);
}
.note > :first-child { margin-top: 0; }
.note > :last-child { margin-bottom: 0; }
.note strong, .note b { color: var(--text); }

/* ── Forms ────────────────────────────────────────────────────────────────── */
.form { max-width: 460px; }
.field { margin-bottom: var(--sp-4); }
.field__label {
  display: block;
  margin-bottom: var(--sp-2);
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.field__hint {
  display: block;
  margin-top: var(--sp-2);
  font-size: 13px;
  color: var(--text-dim);
}

.input {
  display: block;
  width: 100%;
  min-height: 46px;
  padding: 11px 14px;
  font: inherit;
  font-size: 16px; /* 16px keeps iOS Safari from zooming the page on focus */
  color: var(--text);
  background: var(--ink-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r-md);
  transition: border-color .18s var(--ease), background-color .18s var(--ease);
}
.input::placeholder { color: var(--text-dim); }
.input:hover { border-color: var(--line-lit); }
.input:focus-visible { border-color: var(--s5); background: var(--ink-3); }
textarea.input { min-height: 120px; resize: vertical; line-height: 1.55; }

.checkbox {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  min-height: 44px;
  padding: 6px 0;
  font-size: 15px;
  color: var(--text-dim);
  cursor: pointer;
}
/* Drawn from scratch. accent-color alone leaves Chrome deriving the UNCHECKED
   box from a light rendering, which paints a solid pale-grey square on #06080C
   — louder than the primary button and the first thing the eye lands on, on the
   one required control of the sign-up form. */
.checkbox input[type='checkbox'] {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  flex: none;
  width: 20px;
  height: 20px;
  margin: 2px 0 0;
  background: var(--ink-2);
  border: 1px solid var(--line-2);
  border-radius: 5px;
  cursor: pointer;
  transition: border-color .18s var(--ease), background-color .18s var(--ease);
}
.checkbox input[type='checkbox']:hover { border-color: var(--line-lit); }
.checkbox input[type='checkbox']:checked {
  /* Ring, not fill: at 20px a full spectrum square reads as noise next to the
     label. Same two-layer trick as the primary button, and the tick is white so
     it never has to survive six backgrounds. */
  border-color: transparent;
  background-color: transparent;
  background-image: linear-gradient(var(--ink-2), var(--ink-2)), var(--spectrum-2x);
  background-size: auto, 200% 100%;
  background-position: 0 0, 0% 50%;
  background-origin: padding-box, border-box;
  background-clip: padding-box, border-box;
}
/* The tick. The interior the rule above paints is --ink-2, not the spectrum, so
   the tick is drawn in --text: #E9EFF7 on #121826 is about 14:1, where --ink-0
   on that same interior measured 1.12:1 and was invisible at 20px. */
.checkbox input[type='checkbox']:checked::after {
  content: '';
  position: absolute;
  left: 6px;
  top: 2px;
  width: 5px;
  height: 10px;
  border: solid var(--text);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.checkbox input[type='checkbox']:disabled { opacity: .45; cursor: not-allowed; }
.checkbox a { color: var(--text); }

.form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  margin-top: var(--sp-5);
}

/* Whole-surface tints in the relevant spectrum hue. No stripes. */
.alert {
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line-2);
  border-radius: var(--r-md);
  background: var(--ink-2);
  font-size: 15px;
  color: var(--text);
  margin-bottom: var(--sp-4);
}
.alert > :last-child { margin-bottom: 0; }
.alert--ok   { background: rgba(76, 224, 160, .10);  border-color: rgba(76, 224, 160, .38); }
.alert--err  { background: rgba(255, 77, 109, .10);  border-color: rgba(255, 77, 109, .40); }
.alert--info { background: rgba(58, 199, 255, .10);  border-color: rgba(58, 199, 255, .38); }
.alert:empty { display: none; }

/* Bot bait. Off-screen rather than display:none, which is trivially detected. */
.honeypot {
  position: absolute !important;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
  padding: 0;
  margin: -1px;
}

/* ── Footer ───────────────────────────────────────────────────────────────── */
/* FRUNKSHOW'S FOOTER, not a second site's. It carried five numbered channel
   headings, a tick lane down every gutter, a KeoCodes column, four legal links
   belonging to another site and a coloured tick over each of four status cells.
   Three groups of three now, one status readout, one line home, and space
   instead of texture between them. The spectrum rail returns once, as the
   hairline on top. */
.site-foot {
  position: relative;
  margin-top: var(--sp-8);
  background: var(--ink-1);
  border-top: 1px solid var(--line);
  color: var(--text-dim);
  font-size: 15px;
}
.site-foot::before {
  content: '';
  position: absolute;
  top: -1px;
  left: 0;
  right: 0;
  height: 2px;
  background-image: var(--spectrum-2x);
  background-size: 200vw 100%;
  background-repeat: repeat-x;
  background-position-x: 0;
}

/* ONE LINE: a short heading and the two buttons, on one row from 720px up. The
   mono "00 START HERE" cue above the heading was a label on a label, and the
   sentence under it repeated the first fact in the status row further down. */
.site-foot__cta {
  border-bottom: 1px solid var(--line);
  padding-block: var(--sp-5);
}
.site-foot__cta-title {
  font-size: 19px;
  font-weight: 700;
  font-stretch: 106%;
  letter-spacing: -.01em;
  line-height: 1.25;
  margin: 0;
}
.site-foot__cta-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: var(--sp-4);
}

@media (min-width: 720px) {
  .site-foot__cta-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-6);
  }
  .site-foot__cta-actions { margin-top: 0; flex: none; }
}

.site-foot__grid {
  display: grid;
  gap: var(--sp-6);
  padding-block: var(--sp-7);
}
/* Brand + three groups. Space between the columns, not a tick lane down each
   gutter: a repeating hairline pattern in every gap is texture the footer does
   not need, and it was the thing making four short link lists read as a
   sixteen-row control panel. */
@media (min-width: 720px) {
  .site-foot__grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--sp-6) var(--sp-6);
  }
}
/* Brand + TWO groups. The Tools column is gone: the nav carries all three tool
   links on every page and the CTA band above carries two more, so the column was
   the fifth and the self-link — on /light-show it linked /light-show. Track
   counts follow frunkshow_footer_groups(); change them together. */
@media (min-width: 1040px) {
  .site-foot__grid { grid-template-columns: 1.6fr repeat(2, minmax(0, 1fr)); gap: var(--sp-7); }
}

.site-foot__brand .wordmark { font-size: 19px; }
.site-foot__brand p {
  margin: var(--sp-3) 0 0;
  max-width: 30ch;
  font-size: 14px;
  color: var(--text-dim);
}

/* Each group is a <details open>. Below 720px it is a real disclosure, so five
   stacked lists are not a wall of scroll on a phone. From 720px up it is forced
   open and the summary stops behaving like a control — the !important pair
   holds the content visible even against a UA that uses content-visibility to
   hide it, so a stray keyboard toggle cannot empty a column.
   ⚠ pointer-events only stops the MOUSE. A <summary> stays focusable, still
   announces as an expandable disclosure and still toggles on Enter or Space, so
   above the breakpoint that would be five inert controls in the tab order of
   every page. ui.js takes them out of it (and puts them back below 720px);
   this rule is the visual half of the same decision. */
/* A phone gets one hairline over each group so the disclosures read as a stack
   of rows. A wide screen gets nothing at all — the columns are separated by the
   grid gap, which is what a gap is for. */
.foot-group { border-top: 1px solid var(--line); }
.foot-group__heading {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 9px;
  min-height: 44px;
  font-size: 12px;
  font-weight: 650;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.foot-group__heading::-webkit-details-marker { display: none; }
.foot-group__heading::after {
  content: '';
  margin-left: auto;
  width: 8px;
  height: 8px;
  border-right: 1.5px solid var(--text-dim);
  border-bottom: 1.5px solid var(--text-dim);
  transform: rotate(45deg) translateY(-2px);
  transition: transform .2s var(--ease);
}
.foot-group[open] > .foot-group__heading::after { transform: rotate(-135deg) translateY(-2px); }

.foot-group__links { list-style: none; margin: 0 0 var(--sp-3); padding: 0; }
.foot-group__links li { margin: 0; }
/* 44px on a phone. Twenty-odd of these stack in the footer of every page, four
   of them the legal links, and the disclosure summaries directly above them are
   already 44px — a 40px row here stepped the target size DOWN exactly where the
   thumb has the least room. The >=720px block below takes it back to a dense
   desktop list. */
.foot-group__links a {
  display: flex;
  align-items: center;
  padding: 11px 0;
  min-height: 44px;
  font-size: 14px;
  color: var(--text-dim);
  text-decoration: none;
}
.foot-group__links a:hover { color: var(--text); text-decoration: underline; text-decoration-color: var(--s5); }

@media (min-width: 720px) {
  /* No rule and no lane between columns above the breakpoint: the gap is the
     separation. */
  .foot-group { border-top: 0; }
  .foot-group__heading { pointer-events: none; cursor: default; min-height: 0; margin-bottom: var(--sp-3); }
  .foot-group__heading::after { display: none; }
  .foot-group__links {
    display: block !important;
    content-visibility: visible !important;
    margin-bottom: 0;
  }
  /* ⚠ THE RULE ABOVE IS NOT ENOUGH ON ITS OWN. Chrome hides a closed
     <details> by putting content-visibility: hidden on the ::details-content
     SLOT, not on the child list, so no amount of !important on
     .foot-group__links reaches it: rendered with scripting off against a closed
     <details> the columns came out empty — headings, tick lanes and nothing
     else. The markup therefore ships open and ui.js closes the groups below
     this breakpoint; this rule is the belt to that braces, so a stray toggle at
     desktop width cannot empty a column either. Harmless where the pseudo is
     not supported — those browsers never hid the list this way. */
  .foot-group::details-content {
    content-visibility: visible !important;
    display: block !important;
  }
  .foot-group__links a { padding: 5px 0; min-height: 0; }
}

/* THE STATUS ROW. Four facts about this build, in mono, stated in the product's
   own vocabulary — the phase, the car, the frame grid, and what does and does
   not leave the visitor's device. It is the one part of the chrome that could
   not be lifted onto another site, because none of it is true of anything else.
   Values come from frunkshow_status_facts() in includes/shell.php.

   It used to light each cell with its own 2px segment of spectrum and rule a
   vertical tick lane between the columns on a wide screen — eight decorations
   around four short sentences that are already the most distinctive thing in
   the footer. The facts carry themselves; the mono is the signal. */
.foot-status {
  display: grid;
  gap: var(--sp-5);
  margin: 0;
  padding-block: var(--sp-6);
  border-top: 1px solid var(--line);
}
.foot-status__label {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.foot-status__value {
  margin: var(--sp-2) 0 0;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
  max-width: 40ch;
}
@media (min-width: 720px) {
  .foot-status { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1040px) {
  .foot-status { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--sp-6); }
}

.site-foot__legal {
  padding-block: var(--sp-5);
  border-top: 1px solid var(--line);
  font-size: 14px;
  line-height: 1.65;
}
/* THE MEASURE, not 90ch. At 13px that resolved to 655px — about 96 characters a
   line, the LONGEST measure on a site that caps body copy at 68 characters, and
   it was spent on the smallest, dimmest type on the page. The inverse of the
   hierarchy. */
.site-foot__legal p { max-width: var(--measure); color: var(--text-dim); }
.site-foot__legal a { color: var(--text); }

.site-foot__notice {
  font-size: 13px;
  color: var(--text-dim);
  max-width: var(--measure);
}

.site-foot__copy {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--sp-5);
  align-items: center;
  justify-content: space-between;
  padding-block: var(--sp-4) var(--sp-6);
  border-top: 1px solid var(--line);
  font-size: 13px;
  color: var(--text-dim);
}
.site-foot__copy p { margin: 0; }

/* THE ONE LINE HOME, and the only link in the footer that leaves FrunkShow other
   than the copyright-complaints route. It is a standalone target rather than a
   link inside a sentence, so it takes the 44px floor on a phone. */
.site-foot__by a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--text-dim);
  text-decoration-color: var(--line);
}
.site-foot__by a:hover { color: var(--text); text-decoration-color: var(--s5); }
@media (min-width: 720px) {
  /* 24px, not 0: still the AA floor for a standalone target. */
  .site-foot__by a { min-height: 24px; }
}

/* ── Utility ──────────────────────────────────────────────────────────────── */
/* ⚠ THE BACK-TO-TOP BUTTON IS GONE, markup and all. It was a 48px
   spectrum-ringed circle fixed over the page, looping its gradient forever,
   sitting at x436-484 on a 500px viewport whose content column ends at x480 —
   on top of the text — and every destination it could reach is in the footer it
   was floating above. ui.js already guards `if (toTop)`, so the script is
   unaffected by its absence. */

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 12px 18px;
  background: var(--ink-2);
  border: 1px solid var(--s5);
  border-radius: 0 0 var(--r-md) 0;
  color: var(--text);
  text-decoration: none;
  font-weight: 600;
}
.skip-link:focus { left: 0; }

.sr-only, .visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── Motion ───────────────────────────────────────────────────────────────── */
/* There are no looping animations left on the site — every spectrum surface is
   pinned to a whole red-to-violet pass at rest — so this block now covers only
   the two modal keyframes and the site's hover/focus transitions. The per-
   surface background-position pins that used to live here are unconditional
   declarations on the elements themselves, which is why they are not repeated. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Print: the ground inverts so a USB guide printed for the garage is readable. */
@media print {
  /* .foot-status is hidden rather than inverted: its values are pinned to
     --text, which is white on paper. */
  .site-head, .site-foot__cta, .site-foot__grid, .foot-status, .skip-link { display: none !important; }
  body { background: #fff; color: #000; }
  a { color: #000; }
}

/* ── Disclaimers ────────────────────────────────────────────────────────────
   The small print at the foot of every page, rendered by
   frunkshow_footer_disclaimers() in includes/frunkshow-legal.php.

   THE ALIGNMENT RULE. Numbered footnotes and unnumbered general paragraphs
   share one left text edge (--fn-gutter), with the numerals HANGING in the
   gutter to the left of it. On the apex the ol indents its text and the
   paragraphs below it do not, so the block steps in and back out again and
   reads like a mistake. Here every line of small print starts on the same
   vertical, numbered or not, and the only thing in the gutter is the number.

   Quiet by construction: 13px, --text-dim, above the AA floor but visibly
   below body copy. Nothing in here glows — it is the one part of the page that
   is not supposed to attract the eye.
   ────────────────────────────────────────────────────────────────────────── */
/* Padding-free by default. The block normally lands inside the footer's own
   legal container, which is already inside a .wrap — giving it a gutter here
   would pay for the gutter twice and step the small print in from every other
   line on the page. The one selector below covers the other case: dropped bare
   into <main> or <footer>, it centres and pads itself rather than running to
   the screen edge. */
.disclaimers {
  --fn-gutter: 2.1em;
  font-size: 13px;
  line-height: 1.7;
  color: var(--text-dim);
}
.site-main > .disclaimers,
.site-foot > .disclaimers {
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-block: var(--sp-5);
}

.disclaimers ol.fn-list {
  list-style: decimal;
  margin: 0 0 var(--sp-4);
  padding: 0 0 0 var(--fn-gutter);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}
.disclaimers ol.fn-list li { margin: 0; padding: 0; max-width: var(--measure); }
/* Mono numerals, so the one piece of data in the block reads as data (motif 4)
   and a two-digit note still lines up with a one-digit one. */
.disclaimers ol.fn-list li::marker {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: .92em;
  color: var(--text-dim);
}
/* The general paragraphs pick up the same left edge the list text sits on —
   BUT ONLY WHEN THERE IS A LIST. The gutter exists so a numeral has somewhere
   to hang; on the eight pages that render no footnotes it held nothing, and
   three paragraphs of legal copy stepped 27px right of the footer wordmark, the
   channel headings, the status ticks and the copyright line, all of which start
   on the wrap edge. That in-and-out is the exact thing this block was written
   to stop the apex footer doing. :has() is the whole condition: with no
   ol.fn-list the paragraphs sit on the footer's own left vertical. */
.disclaimers p {
  margin: 0 0 var(--sp-3);
  padding-left: 0;
  max-width: var(--measure);
}
.disclaimers:has(ol.fn-list) p { padding-left: var(--fn-gutter); }
.disclaimers p:last-child { margin-bottom: 0; }

.disclaimers a { color: var(--text); text-decoration: underline; text-decoration-color: var(--line-lit); }
.disclaimers a:hover { text-decoration-color: var(--s5); }

/* The ↵ back to the superscript. A standalone control, so it takes the 24px
   AA target rather than the 8px the glyph occupies on its own — but no side
   padding on top of that, or the arrow floats away from the sentence it
   belongs to and reads as the start of the next line.
   ⚠ --text-dim, NOT --text-faint. The faint value is 4.05:1 on --ink-1, which
   is fine for the nav and footer indices (aria-hidden decoration) but not for
   this: it is a real <a> with a visible glyph and an aria-label, so 1.4.3's
   4.5:1 applies. --text-dim is 7.31:1 and still visibly quieter than the note
   body it follows. */
.disclaimers .fn-back {
  display: inline-block;
  min-width: 24px;
  height: 24px;
  line-height: 24px;
  margin-left: 1px;
  vertical-align: -7px;
  text-align: center;
  text-decoration: none;
  color: var(--text-dim);
}
.disclaimers .fn-back:hover { color: var(--s5); }

/* Landing on a footnote from its superscript. The tint covers the note's own
   text box and stops there; an earlier version bled it left to swallow the
   numeral too and, because the marker is outside the content box, that read as
   a second rounded blob floating beside the note rather than one highlight.
   The numeral is lit instead. */
.disclaimers .fn-list li:target {
  color: var(--text);
  background: rgba(58, 199, 255, .10);
  border-radius: var(--r-sm);
  box-shadow: 0 0 0 6px rgba(58, 199, 255, .10);
}
.disclaimers .fn-list li:target::marker { color: var(--s5); }

/* The superscript itself, in page copy. Sized off the surrounding text so it
   stays a superscript in a 34px heading and in a 15px paragraph alike, and
   line-height 0 keeps it from opening the line it sits on. */
.fn-ref {
  font-size: .62em;
  font-weight: 700;
  line-height: 0;
  vertical-align: super;
}
.fn-ref a {
  color: var(--s5);
  text-decoration: none;
  padding: 0 0 0 1px;
}
.fn-ref a:hover { text-decoration: underline; }

@media print {
  .disclaimers { color: #000; }
  .disclaimers a { color: #000; }
  .disclaimers .fn-back { display: none; }
}

/* ── Modal ──────────────────────────────────────────────────────────────────
   THE SIGN-UP DIALOG on /account, and the only modal on the site.

   THREE STATES, and the default is the one that matters most:

     · no state class — display:none. This is what a visitor with scripting off
       sees on a plain /account: nothing. The panel is in the markup so ui.js
       can open it without a page load, and it stays out of sight and out of the
       tab order until somebody asks for it.
     · .is-inline — PHP writes this when the URL asked for the form
       (/account?create=1, or a rejected sign-up coming back as ?f=register).
       The panel renders as an ordinary block in the page flow: no overlay, no
       backdrop, no close button, nothing to trap anyone. This is the whole
       no-JavaScript path, and it is why the trigger is a real <a href> rather
       than a button.
     · .is-open — ui.js has portalled the block out of <main> onto <body>,
       written role="dialog" aria-modal="true", made the rest of the page inert
       and moved focus in. Escape, the close button and a backdrop click all
       leave; Tab is trapped until one of them does.

   No shadow (motif 3): the panel is lit by its spectrum tick and separated from
   the page by the darkened, blurred ground behind it.
   ────────────────────────────────────────────────────────────────────────── */
.modal { display: none; }

/* No-JS / pre-script state: a panel on the page, not over it. */
.modal.is-inline {
  display: block;
  margin-top: var(--sp-5);
}
.modal.is-inline .modal__panel {
  padding: var(--sp-5);
  border: 1px solid var(--line-card);
  border-radius: var(--r-lg);
  background: var(--ink-1);
}
/* There is nothing to close: the panel IS the page here. A dismiss control that
   dismisses nothing is worse than no control. */
.modal.is-inline .modal__close { display: none; }

.modal.is-open {
  display: flex;
  position: fixed;
  inset: 0;
  z-index: 80;
  /* Scrolls as a whole rather than scrolling the panel's insides: with a phone
     keyboard up, an inner scroller strands the submit button under it. */
  align-items: flex-start;
  justify-content: center;
  padding: var(--sp-5) var(--gutter);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: rgba(6, 8, 12, .82);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  animation: frunkshow-modal-in .18s var(--ease);
}
.modal.is-open .modal__panel {
  position: relative;
  width: 100%;
  /* .form caps at 460px; the extra is the panel's own padding, so the field
     column inside is the same width it is everywhere else on the site. */
  max-width: 516px;
  margin: auto;
  padding: var(--sp-5);
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  background: var(--ink-1);
  animation: frunkshow-modal-rise .2s var(--ease);
}

/* The card tick at dialog scale, but the FULL spectrum rather than one hue: a
   dialog has no place in the .cards hue cycle, so it takes the whole rail. */
.modal__title {
  margin: 0 0 var(--sp-2);
  padding-right: 44px;   /* clears the close button */
  font-size: 22px;
  font-weight: 750;
  font-stretch: 110%;
  letter-spacing: -.015em;
  line-height: 1.2;
}
.modal__title::before {
  content: '';
  display: block;
  width: 28px;
  height: 2px;
  margin-bottom: var(--sp-3);
  border-radius: 2px;
  background-image: var(--spectrum);
}
.modal__lede { color: var(--text-dim); margin-bottom: var(--sp-4); }

.modal__close {
  position: absolute;
  top: var(--sp-3);
  right: var(--sp-3);
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  font: inherit;
  color: var(--text-dim);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: color .18s var(--ease), border-color .18s var(--ease), background-color .18s var(--ease);
}
.modal__close:hover { color: var(--text); background: var(--ink-2); border-color: var(--line-2); }
.modal__close svg { display: block; }

/* Set by ui.js while a dialog is open, so the page behind cannot be scrolled
   out from under it. */
body.has-modal { overflow: hidden; }

@keyframes frunkshow-modal-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes frunkshow-modal-rise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* ── Conversations list (account) ───────────────────────────────────────────
   Added for the Conversations card on /account. The only list on the site
   whose rows are links into a private thread, so it is the only thing here the
   existing components did not already cover: .kv is label/value, .faq is a
   disclosure, .cards is a grid of panels. Everything else on that card — the
   panel, the heading, the foot note — is .card and .muted as they are.

   Row = one link, stacked title over meta, hairline between rows. The whole
   row is the target and clears 44px on a phone. Focus is the site-wide
   :focus-visible ring; nothing here removes an outline. */
.threads { list-style: none; margin: 0; padding: 0; }
.thread + .thread { border-top: 1px solid var(--line); }

.thread__link {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 11px 0;
  min-height: 44px;
  justify-content: center;
  color: var(--text);
  text-decoration: none;
}
/* Where the row came from, above the words themselves. Mono and dim so it sits
   under the quoted line rather than competing with it — it is a label on the
   content, not the content. */
.thread__from {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--text-dim);
}
/* The title carries the underline, not the meta line: an underlined date in
   mono reads as a second link rather than as the row's own timestamp. It is the
   first line of whatever they typed, so it can arrive as one unbroken
   80-character string — it wraps mid-word rather than pushing the card wider
   than the page. */
.thread__line {
  font-weight: 550;
  overflow-wrap: anywhere;
  text-decoration: underline;
  text-decoration-color: var(--line-lit);
  text-decoration-thickness: 1px;
  text-underline-offset: .18em;
  transition: text-decoration-color .18s var(--ease);
}
.thread__link:hover { color: #fff; }
.thread__link:hover .thread__line { text-decoration-color: var(--s5); }

/* Data in mono (motif 4) — the date and the state are facts about the thread. */
.thread__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px 8px;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  font-size: 12px;
  letter-spacing: -.01em;
  color: var(--text-dim);
}
/* An answer sitting there unread. Green — the one state on the site carried by
   colour rather than by words, and it is paired with the word "New" beside it. */
.thread__state--new { color: var(--s4); }

/* The site-wide print block inverts the ground and pins `a` to #000, but
   .thread__link is more specific than `a` — without this the rows would print
   white on white. The green state pin goes with them. */
@media print {
  .thread__link,
  .thread__state--new { color: #000; }
  /* --text-dim is a light grey pitched for #06080C; on paper it is nearly gone.
     Mid-grey keeps the label and the stamp secondary without making them
     unreadable. */
  .thread__from,
  .thread__meta { color: #444; }
}

/* ══ Magic-link conversation (/message) ═══════════════════════════════════════
   The one component the console did not already have: a transcript. Everything
   else on that page is stock — .section, .wrap--narrow, .stack, .lane, .form,
   .field, .input, .alert, .btn, .honeypot, .muted.

   Two things are being drawn here. The bubbles, which are cards that have to
   say WHO WROTE THEM at a glance without turning into a chat app; and the
   attachment chips, whose class names are fixed by tf_render_files_html() in
   admin/includes/thread-files.php (.msg-files, .msg-file-thumb, .msg-file-chip
   and its three children) — that helper emits the same markup for the apex page
   and this one, so these five selectors are named by it, not chosen here.

   The two voices are separated by a whole-surface tint in one fixed spectrum
   hue, never a side stripe, and never the .stack hue cycle: dealing a different
   colour to every bubble in sequence would make the colour mean "position in
   the list" when the only thing it may mean here is "who is speaking". */

/* The reply form runs the full measure. .form caps at 460px, which is right
   under a contact heading and wrong under a ~680px transcript — the box would
   read as an aside rather than the answer to what is above it. */
.msg-form { max-width: none; }

.msg-thread {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

/* The visitor's own voice: the resting card, same edge and ground as .card. */
.msg-bubble {
  padding: var(--sp-4);
  border: 1px solid var(--line-card);
  border-radius: var(--r-lg);
  background: var(--ink-1);
}
/* Mine. The same tint recipe as .alert--info — --s5 at .10 over a .38 border —
   because it is the same job: a surface the eye separates from the one next to
   it while both stay plainly readable. */
.msg-bubble--mine {
  border-color: rgba(58, 199, 255, .38);
  background: rgba(58, 199, 255, .10);
}
/* A machine-written milestone. Quieter than either person on purpose: it is a
   record of something that happened, not somebody talking. Listed after --mine
   so it wins on a bubble that somehow carries both. */
.msg-bubble--system {
  border-color: var(--line);
  background: transparent;
}

/* Who and when, in mono — a timestamp is data. */
.msg-bubble__meta {
  margin: 0 0 var(--sp-2);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  font-size: 12px;
  letter-spacing: -.01em;
  color: var(--text-dim);
}
.msg-bubble__who { color: var(--text); font-weight: 600; }
.msg-bubble--system .msg-bubble__who { color: var(--text-dim); font-weight: 500; }

/* pre-wrap, so paragraph breaks somebody typed survive; wrap-anywhere so a
   pasted URL or a 200-character word cannot push the page sideways. */
.msg-bubble__body {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  line-height: 1.6;
  color: var(--text);
}
.msg-bubble__body:last-child { margin-bottom: 0; }

/* ── Attachments (class names owned by tf_render_files_html) ──────────────── */
.msg-files {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
}
.msg-file-thumb {
  display: block;
  line-height: 0;
  border: 1px solid var(--line-2);
  border-radius: var(--r-md);
  overflow: hidden;
}
.msg-file-thumb:hover { border-color: var(--line-lit); }
.msg-file-thumb img { display: block; width: 128px; height: 96px; object-fit: cover; }

/* 44px tall so a chip is a real touch target, not a line of text with a box. */
.msg-file-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  max-width: 100%;
  padding: 8px 14px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  background: var(--ink-2);
  color: var(--text);
  font-size: 14px;
  text-decoration: none;
}
.msg-file-chip:hover { border-color: var(--line-lit); background: var(--ink-3); }
.msg-file-chip__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-file-chip__size {
  flex: none;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--text-dim);
  white-space: nowrap;
}

/* The file input. .input gives it the box; this gives its native button the
   same treatment .btn--ghost has, because the UA default is a light-derived
   control that lands on #06080C as the brightest thing on the page. */
.msg-file-input {
  padding: 8px 10px;
  line-height: 1.5;
  cursor: pointer;
}
.msg-file-input::file-selector-button {
  margin-right: 10px;
  min-height: 28px;
  padding: 4px 12px;
  font: inherit;
  font-size: 14px;
  font-weight: 650;
  color: var(--text);
  background: transparent;
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  cursor: pointer;
  transition: border-color .18s var(--ease), background-color .18s var(--ease);
}
.msg-file-input::file-selector-button:hover { background: var(--ink-1); border-color: var(--line-lit); }

/* ══ Waveform + selection (/lock-sound, built by js/waveform.js) ══════════════
   The only canvas component on the site. The bars, the baseline and the ruler
   ticks are painted in JS (it reads --text, --text-faint, --line and
   --line-tick off .wf, so the picture stays on these tokens); everything you
   can touch is a DOM element styled here.

   THE WAVEFORM IS THE WIDTH OF THE TOOL, not of the .wrap. It starts on the
   page's one left vertical and ends on --measure, the same right vertical as
   the size readout, the prose and the file picker. Left full-bleed it drew
   1080px of canvas at 1280 with a 460px cluster of controls tucked under its
   bottom-left corner and 600px of empty ground beside everything else — a
   picture and a form, rather than one instrument. A 44px handle target centred
   on 0 s would hang half of itself outside that box, so waveform.js slides the
   target back inside and moves the drawn line WITHIN it; the line still lands
   exactly on the sample. That is why the handle's <i> is positioned from JS.

   Nothing here is elevated and nothing casts a shadow: the selected audio is
   brighter than the audio around it, and the two edges carry the spectrum as a
   line, which is how this site marks the live thing. */

.wf {
  --wf-h: 120px;
  max-width: var(--measure);
  margin-block: var(--sp-5);
}
@media (min-width: 720px) {
  .wf { --wf-h: 156px; }
}

.wf__surface {
  position: relative;
  height: var(--wf-h);
  /* A drag must move the selection, never scroll the page. Pointer Events are
     the only input path in waveform.js and this is the half that makes them
     behave the same on a phone as on a trackpad. */
  touch-action: none;
}

.wf__canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* The middle of the selection: a slide target, drawn with nothing at all. The
   brightness change in the canvas already says where it is. */
.wf__region {
  position: absolute;
  inset-block: 0;
  left: 0;
  cursor: grab;
  touch-action: none;
}
.wf.is-dragging .wf__region { cursor: grabbing; }

/* 44px wide, 2px visible. The box is the touch target and the focus ring; the
   <i> inside is the line you see. */
.wf__handle {
  position: absolute;
  inset-block: 0;
  left: 0;
  width: 44px;   /* HANDLE_W in waveform.js — change both together */
  cursor: ew-resize;
  touch-action: none;
}
.wf__handle[aria-disabled="true"] { display: none; }

/* `left` is written by waveform.js, not here: it is the offset of the edge
   inside the clamped touch target, which is half the target everywhere except
   at the two ends of the file. */
.wf__handle > i {
  position: absolute;
  inset-block: 0;
  left: 22px;
  width: 2px;
  margin-left: -1px;
  /* The six spectrum stops running top to bottom — the site's --spectrum token
     is a 90deg gradient and this line is vertical, so the stops are restated
     rather than rotated. A LINE, never a fill. */
  background-image: linear-gradient(180deg,
      var(--s1), var(--s2), var(--s3), var(--s4), var(--s5), var(--s6));
  border-radius: 1px;
  /* ⚠ THE CASING IS WHAT KEEPS THE EDGE FINDABLE. Uncased, the yellow and
     green thirds of this gradient sit at about 1.1:1 and 1.4:1 against the
     selected waveform, which is painted in --text — so on a dense chime the
     middle of the line simply disappears into the bars and only the red top
     and the violet foot survive. One pixel of the page's own ground on each
     side separates it from both the bright bars and the dim ones, and adds no
     fill of its own. */
  box-shadow: 0 0 0 1px var(--ink-0);
}
/* Hover and drag widen the bloom, not the line, so the edge never moves under
   the pointer. It inherits the same vertical gradient rather than taking
   --spectrum-soft: that token is a 90deg gradient, and across an 8px-wide box
   it resolves to one flat hue instead of a glow the colour of the line. */
.wf__handle > i::after {
  content: '';
  position: absolute;
  inset-block: 0;
  left: -3px;
  right: -3px;
  background-image: inherit;
  opacity: 0;
  transition: opacity .18s var(--ease);
}
.wf__handle:hover > i::after,
.wf__handle:focus-visible > i::after,
.wf.is-dragging .wf__handle > i::after { opacity: .4; }

/* The grip: a short neutral nub at the top of the edge, so a handle reads as
   something you can take hold of. Neutral, not spectrum — six stops squeezed
   into ten pixels is noise, and the line under it already carries them. */
.wf__handle > i::before {
  content: '';
  position: absolute;
  z-index: 1;
  top: 0;
  left: -4px;
  right: -4px;
  height: 6px;
  background: var(--text);
  border-radius: 3px;
}

/* The site's :focus-visible ring is a 2px outline with a bloom; on a 44px box
   inset over a waveform it needs to hug the target rather than sit 2px outside
   it, where it would overlap the other handle on a short selection. */
.wf__handle:focus-visible {
  outline-offset: -2px;
  border-radius: var(--r-sm);
}

/* Where playback has reached. --s3 is the brightest stop, and it is the one
   spectrum colour not used by anything else on this page. */
.wf__playhead {
  position: absolute;
  inset-block: 0;
  left: 0;
  width: 2px;
  margin-left: -1px;
  background: var(--s3);
  pointer-events: none;
  transition: transform .06s linear;
}
/* The playhead is set from a rAF loop, so the easing is a smoothing pass on top
   of motion the user did not ask for. It goes. */
@media (prefers-reduced-motion: reduce) {
  .wf__playhead { transition: none; }
}
