/* ============================================================================
   Sonlit — sonlit.keocodes.com
   "The instrument, at night."

   Sonlit 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: 68px;   /* tall state; the compact state is ~52px */
  --wrap: 1120px;
  --gutter: 20px;
}

/* ── 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; }
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 ───────────────────────────────────────────────────────────────── */
.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.wrap--narrow { max-width: 720px; }

.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-4); }

.rule { height: 1px; border: 0; background: var(--line); margin-block: var(--sp-6); }

/* 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. */
.lane {
  height: 10px;
  margin-block: var(--sp-6);
  background-repeat: no-repeat;
  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 > .rule, .site-main > .lane, .site-main > .alert,
.site-main > .eyebrow, .site-main > .lede {
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* ── 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. */
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 {
  font-size: clamp(34px, 7vw, 60px);
  font-weight: 800;
  font-stretch: 118%;
  letter-spacing: -.02em;
}
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: 68ch; }
p:last-child { margin-bottom: 0; }

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

.eyebrow {
  display: block;
  font-size: 12px;
  font-weight: 650;
  font-stretch: 108%;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 0 0 var(--sp-3);
}

/* 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. */
.spectrum-text {
  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: 68ch; }
li + li { margin-top: var(--sp-2); }

/* ── Header ───────────────────────────────────────────────────────────────── */
/* Sticky, and it SHRINKS on scroll: .is-compact reduces the padding and the
   wordmark rather than swapping layouts, so nothing jumps. The rail pinned to
   its bottom edge is the scroll-progress indicator. */
.site-head {
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(6, 8, 12, .88);
  -webkit-backdrop-filter: saturate(150%) blur(12px);
  backdrop-filter: saturate(150%) blur(12px);
  border-bottom: 1px solid var(--line);
}

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

/* 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%);
  background-image: var(--spectrum);
  background-size: 100vw 100%;
  background-repeat: no-repeat;
  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: 22px;
  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: 38px;
  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: 2px var(--sp-1);
}
.site-nav a {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 10px;
  font-size: 15px;
  font-weight: 550;
  color: var(--text-dim);
  text-decoration: none;
  border-radius: var(--r-sm);
  transition: color .18s var(--ease), background-color .18s var(--ease);
}
.site-nav a:hover { color: var(--text); background: var(--ink-2); }
/* The rail again, at nav scale. */
.site-nav a.is-active { color: var(--text); }
.site-nav a.is-active::after {
  content: '';
  position: absolute;
  left: 10px;
  right: 10px;
  bottom: 6px;
  height: 2px;
  border-radius: 2px;
  background-image: var(--spectrum);
}

.nav-toggle {
  display: none;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 12px;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  background: var(--ink-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  cursor: pointer;
}
.nav-toggle:hover { border-color: var(--line-lit); background: var(--ink-3); }
.nav-toggle__bars {
  display: block;
  width: 16px;
  height: 2px;
  background: currentColor;
  box-shadow: 0 -5px 0 currentColor, 0 5px 0 currentColor;
}

@media (max-width: 719.98px) {
  :root[data-nav='ready'] .nav-toggle { display: inline-flex; }
  :root[data-nav='ready'] .site-head__inner { flex-wrap: wrap; }
  :root[data-nav='ready'] .site-nav {
    display: none;
    flex-basis: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    padding-bottom: var(--sp-3);
  }
  :root[data-nav='ready'] .site-nav.is-open { display: flex; }
  :root[data-nav='ready'] .site-nav a { padding-inline: 12px; }
  :root[data-nav='ready'] .site-nav a.is-active::after { left: 12px; right: auto; width: 28px; }
  /* No JS, or JS that never bound: the nav 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 ─────────────────────────────────────────────────────────────────── */
/* Channel-lane texture behind the copy, fading out downward, plus the light
   bar. Everything is visible at rest — nothing here waits for a scroll
   observer to appear. */
.hero {
  position: relative;
  overflow: hidden;
  padding-block: var(--sp-7) var(--sp-6);
  border-bottom: 1px solid var(--line);
}
.hero::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(90deg, var(--line) 0 1px, transparent 1px var(--step));
  -webkit-mask-image: linear-gradient(180deg, rgba(0, 0, 0, .55), transparent 62%);
  mask-image: linear-gradient(180deg, rgba(0, 0, 0, .55), transparent 62%);
  pointer-events: none;
}
.hero > * { position: relative; }

.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.
   The mask lives on ::after; ::before is an unmasked, blurred smear of the same
   gradient providing the bloom (a filter on the masked layer would be clipped
   back to the mask, killing the glow). */
.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::before,
.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%;
  animation: sonlit-sweep 22s linear infinite;
}
.hero__lights::before {
  filter: blur(26px);
  opacity: .35;
  transform: scaleY(.55);
}
.hero__lights::after {
  -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;
}
@keyframes sonlit-sweep {
  from { background-position: 0% 50%; }
  to   { background-position: 200% 50%; }
}

@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 {
  /* At rest the pill carries the complete spectrum edge to edge — a primary CTA
     that frames only red-orange-yellow is the single most generic thing this
     design could put on a page. The hover slide runs on the doubled gradient so
     a full pass stays in frame the whole way across. */
  background-image: var(--spectrum);
  background-size: 100% 100%;
  background-position: 0% 50%;
  border-color: transparent;
  color: var(--ink-0);
  font-weight: 700;
  transition: background-position .5s var(--ease), box-shadow .18s var(--ease);
}
.btn--primary:hover {
  background-image: var(--spectrum-2x);
  background-size: 200% 100%;
  background-position: 100% 50%;
  color: var(--ink-0);
  box-shadow: 0 0 26px rgba(58, 199, 255, .22);
}

.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. */
.cards { display: grid; gap: var(--sp-4); }
@media (min-width: 720px) {
  .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 2px line the header rail 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. */
.card__head { position: relative; margin-bottom: var(--sp-3); }
.card__head::before {
  content: '';
  display: block;
  width: 28px;
  height: 2px;
  margin-bottom: var(--sp-3);
  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 big product cards. A quiet radial bloom in the card's own hue lives
   behind the content and only fades UP on interaction — no transform, no lift,
   no shadow. */
/* These two ARE links, and the two biggest blocks on the home page. At rest
   they take the component border, not the card border, so a touch user — who
   never sees a hover state — can tell them apart from the reference panels
   further down the same page. */
.card--tool { padding: var(--sp-5); overflow: hidden; border-color: var(--line-2); }
.card--tool::before {
  content: '';
  position: absolute;
  inset: -40% -20% auto -20%;
  height: 200px;
  background: radial-gradient(60% 100% at 50% 100%, var(--hue-soft), transparent 72%);
  opacity: .5;
  pointer-events: none;
  transition: opacity .3s var(--ease);
}
.card--tool:hover, .card--tool:focus-within {
  /* The full stop, not the .45 tint: a hover state that drops the border below
     the 3:1 it had at rest is a regression dressed as a highlight. */
  border-color: var(--hue);
  background: var(--ink-2);
  box-shadow: 0 0 40px -8px var(--hue-soft);
}
.card--tool:hover::before, .card--tool:focus-within::before { opacity: 1; }
.card--tool .card__title { font-size: clamp(21px, 3vw, 26px); font-stretch: 112%; }
/* The footer of a tool card is its call to action, so it has to look like one.
   Without the hue and the arrow it renders as a caption under a hairline and
   gets read as a label for the paragraph above it. */
.card--tool .card__foot { color: var(--hue); font-weight: 650; }
.card--tool .card__foot > span {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.card--tool .card__foot > span::after {
  content: '\2192';
  font-size: 1.1em;
  line-height: 1;
  transition: transform .22s var(--ease);
}
.card--tool:hover .card__foot > span::after,
.card--tool:focus-within .card__foot > span::after { transform: translateX(4px); }

/* A card that is reference material rather than an action. */
.card--quiet {
  background: transparent;
  border-color: var(--line-card);
  padding: var(--sp-4);
}
.card--quiet:hover { border-color: var(--line-2); }

@media (min-width: 720px) {
  .card { padding: var(--sp-6); }
  .card--quiet { padding: var(--sp-5); }
}

/* ── Bits ─────────────────────────────────────────────────────────────────── */
.tag {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 4px 11px 4px 9px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  background: var(--ink-2);
  font-size: 13px;
  font-weight: 550;
  color: var(--text);
  white-space: nowrap;
}
.tag::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--hue);
  flex: none;
}
/* The dot is the whole signal, and it says one thing: is this settled?
   FILLED (plain .tag, in the card's hue) — established, published, decided.
   HOLLOW (.tag--soon) — not yet: unbuilt, unconfirmed, owner-reported.
   .tag--fact is the filled dot pinned to green regardless of the card's place
   in the hue cycle, for the one distinction that must not be decorative: what
   Tesla has published, versus what owners report and I have not checked. */
.tag--soon { color: var(--text-dim); border-color: var(--line-2); }
.tag--soon::before { background: none; border: 1px solid var(--text-dim); }

.tag--fact { color: var(--text); border-color: rgba(76, 224, 160, .55); }
.tag--fact::before { background: var(--s4); border: 0; }

.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: 10px;
}
.kv > dd {
  margin: 0 0 10px;
  padding-bottom: 10px;
  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; }
@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. */
  .kv { grid-template-columns: minmax(140px, 34%) 1fr; }
  .kv > dt { padding: 10px var(--sp-3) 10px 0; border-bottom: 1px solid var(--line); }
  .kv > dd { padding-top: 10px; 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: sonlit-step; list-style: none; padding: 0; margin: 0; max-width: 68ch; }
.step {
  counter-increment: sonlit-step;
  position: relative;
  padding-left: 46px;
  margin: 0 0 var(--sp-5);
}
.step:last-child { margin-bottom: 0; }
.step::before {
  content: counter(sonlit-step, decimal-leading-zero);
  position: absolute;
  left: 0;
  top: 1px;
  width: 32px;
  height: 26px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: var(--ink-2);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--text-dim);
}

/* FAQ */
.faq { border-top: 1px solid var(--line); max-width: 68ch; }
.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 inset aside. Whole-surface tint, never a coloured left stripe. */
.note {
  padding: var(--sp-4);
  /* --ink-1 on --ink-0 is 1.04:1, so the fill does nothing on its own and the
     border is the whole boundary. On several pages this box carries the most
     important sentence there is ("Not built yet."). */
  border: 1px solid var(--line-card);
  border-radius: var(--r-md);
  background: var(--ink-1);
  color: var(--text-dim);
}
.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: 6px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.field__hint {
  display: block;
  margin-top: 6px;
  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 {
  background-image: var(--spectrum);
  border-color: transparent;
}
/* The tick, in the page ground so it reads against every stop of the spectrum. */
.checkbox input[type='checkbox']:checked::after {
  content: '';
  position: absolute;
  left: 6px;
  top: 2px;
  width: 5px;
  height: 10px;
  border: solid var(--ink-0);
  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 ───────────────────────────────────────────────────────────────── */
/* The keocodes.com STRUCTURE — CTA band, brand block, grouped link grid, legal
   row, disclaimers, copyright — with none of its styling. The spectrum rail
   returns one last time 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);
}

.site-foot__cta {
  border-bottom: 1px solid var(--line);
  padding-block: var(--sp-6);
}
.site-foot__cta h2 { margin-bottom: var(--sp-2); }
.site-foot__cta p { color: var(--text-dim); margin-bottom: 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-5);
  padding-block: var(--sp-6);
}
@media (min-width: 720px) {
  .site-foot__grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--sp-6) var(--sp-5);
  }
}
@media (min-width: 1040px) {
  .site-foot__grid { grid-template-columns: 1.5fr repeat(5, minmax(0, 1fr)); }
}

.site-foot__brand .wordmark { font-size: 20px; }
.site-foot__brand p {
  margin: var(--sp-3) 0 0;
  max-width: 34ch;
  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. */
.foot-group { border-top: 1px solid var(--line); }
.foot-group__heading {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 44px;
  font-size: 13px;
  font-weight: 650;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text);
}
.foot-group__heading::-webkit-details-marker { display: none; }
.foot-group__heading::after {
  content: '';
  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) {
  .foot-group { border-top: 0; }
  .foot-group__heading { pointer-events: none; cursor: default; min-height: 0; margin-bottom: var(--sp-2); }
  .foot-group__heading::after { display: none; }
  .foot-group__links {
    display: block !important;
    content-visibility: visible !important;
    margin-bottom: 0;
  }
  .foot-group__links a { padding: 5px 0; min-height: 0; }
}

.site-foot__legal {
  padding-block: var(--sp-5);
  border-top: 1px solid var(--line);
  font-size: 14px;
  line-height: 1.65;
}
.site-foot__legal p { max-width: 90ch; color: var(--text-dim); }
.site-foot__legal a { color: var(--text); }

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

.site-foot__copy {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  align-items: center;
  padding-block: var(--sp-4) var(--sp-6);
  border-top: 1px solid var(--line);
  font-size: 13px;
  color: var(--text-dim);
}
.site-foot__copy a { color: var(--text-dim); }
.site-foot__copy nav { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); }
/* Three standalone links in a row, not links inside a sentence, so the 44px
   floor applies to them the same way it applies to the group lists above.
   Measured at 21px before this. */
.site-foot__copy nav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
@media (min-width: 720px) {
  /* 24px, not 0: still the AA floor for a standalone target, and these three are
     a nav row rather than links inside a sentence. */
  .site-foot__copy nav a { min-height: 24px; }
}

/* ── Utility ──────────────────────────────────────────────────────────────── */
/* Back to top. Hidden with visibility so it is out of the tab order until
   ui.js reveals it; a no-JS visitor never meets a dead button. The border is
   the spectrum, drawn as a border-box gradient behind a padding-box fill. */
.to-top {
  position: fixed;
  right: max(16px, env(safe-area-inset-right));
  bottom: max(16px, env(safe-area-inset-bottom));
  z-index: 60;
  width: 48px;
  height: 48px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid transparent;
  border-radius: 50%;
  background: linear-gradient(var(--ink-2), var(--ink-2)) padding-box, var(--spectrum) border-box;
  color: var(--text);
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
  transition: opacity .2s var(--ease), transform .2s var(--ease), visibility .2s;
}
.to-top.is-visible { opacity: 1; visibility: visible; transform: none; }
.to-top:hover { background: linear-gradient(var(--ink-3), var(--ink-3)) padding-box, var(--spectrum) border-box; }
.to-top svg { display: block; }

.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 ───────────────────────────────────────────────────────────────── */
/* Everything meaningful is already visible at rest, so switching motion off
   removes decoration and nothing else. */
@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;
  }
  /* 0%, not 50%: with --spectrum-2x that pins a complete spectrum in frame.
     A 50% pin lands mid-pass and shows a two-colour smear with no red at one
     end and no violet at the other, which is the whole point of the image. */
  .hero__lights::before,
  .hero__lights::after { animation: none; background-position: 0% 50%; }
}

/* Print: the ground inverts so a USB guide printed for the garage is readable. */
@media print {
  .site-head, .site-foot__cta, .site-foot__grid, .to-top, .skip-link { display: none !important; }
  body { background: #fff; color: #000; }
  a { color: #000; }
}
