/* ============================================================================
 * NEON — an experimental state channel for the host console.
 * INTERNAL / LAB ONLY. Loaded by templates/layout-console-lab.html at /lab/neon.
 * Nothing in the shipping console references this file yet.
 *
 * ---------------------------------------------------------------------------
 * WHY THIS EXISTS, AND WHY IT ISN'T DECORATION
 *
 * DEFAULT — the first thing anyone builds when told "neon": a stack of
 *   text-shadows in cyan and magenta, a synthwave gradient behind it, glow on
 *   every heading. It is the single most-cloned effect on the web.
 * REJECT — three separate reasons, each sufficient:
 *   (a) It is a STYLE borrowed whole, not a decision. Synthwave is somebody
 *       else's brand.
 *   (b) A stack of text-shadows is pure paint. Animating it repaints the layer
 *       every frame, so the effect everyone reaches for is also the one that
 *       drops frames on the machine a host actually runs an event from.
 *   (c) tokens.css sets `--accent-glow: 0 0 #0000` on purpose, with the comment
 *       "so nothing glows", and dashboard.css reserves colour for interaction
 *       states rather than ambient decoration. Glow-everywhere doesn't just look
 *       bad here, it contradicts a written rule.
 * DERIVE — ask what a neon sign is actually FOR. It is not ornament; it is a
 *   status indicator legible from across a room. OPEN. VACANCY. ON AIR. It has
 *   two states, and — this is the part that matters — the OFF state is still a
 *   physical object. Grey glass on a wall. You can read an unlit sign.
 *   That is exactly the shape of the host's problem. The host is not sitting at
 *   the laptop; they are running a wedding. They glance at a screen from two
 *   metres away and need one answer: is this thing live.
 * JUSTIFY — so the rule writes itself. A tube is UNLIT by default and lights
 *   only when the thing it names is true. Colour then carries information, which
 *   means the rationing is enforced by the model rather than by taste, and the
 *   loud version of this effect becomes unbuildable: you cannot light everything
 *   without lying.
 *
 * ---------------------------------------------------------------------------
 * THE TWO RULES THAT KEEP IT HONEST
 *
 * 1. GLOW IS EMISSION, NEVER THE TUBE. The lit tube's own colour is a near-white
 *    core; the colour lives in blurred layers BEHIND it. That is how real neon
 *    reads — a white-hot line inside a coloured envelope — and it is also the
 *    only version that animates on the compositor, because the layers are
 *    separate elements whose OPACITY changes. Blur radius is set once and never
 *    animated. See §8.
 *
 * 2. THE OFF STATE MUST STAY READABLE. An unlit tube carrying real information
 *    (a photo count, a pool name) is still functional text and still owes 4.5:1.
 *    "Off" is communicated by the ABSENCE OF GLOW, never by fading the text
 *    towards the background. Glass base #93979d measures 6.49:1 on the console's
 *    #0f1011 and every mix in §1 is checked against that floor.
 * ========================================================================== */

/* The single driver. Registered so it can be interpolated: unregistered custom
 * properties are strings to the animation engine and snap 0 -> 1 with no
 * transition. inherits:true is deliberate — it makes a master power switch a
 * one-line change on an ancestor, which is how a whole board comes up at once. */
@property --lit {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}

/* ---------------------------------------------------------------------------
 * §1 — TOKENS
 * The ten stops are the Seam spectrum, duplicated on purpose: the console chain
 * (tokens -> components -> host -> dashboard) deliberately does not load
 * seam.css, so there is no shared file to import them from. If the spectrum ever
 * moves in seam.css §2, it has to move here too. Keeping them numerically
 * identical is what stops the marketing site and the console drifting into two
 * different rainbows.
 * ------------------------------------------------------------------------- */
[data-theme="host"] {
  --n1:  #ff3b30;  /* red      */
  --n2:  #ff7a00;  /* orange   */
  --n3:  #ffc400;  /* amber    */
  --n4:  #b4d900;  /* lime     */
  --n5:  #22c55e;  /* green    */
  --n6:  #00c9b1;  /* teal     */
  --n7:  #22a9ff;  /* azure    */
  --n8:  #4f6bff;  /* blue     */
  --n9:  #9d5cff;  /* violet   */
  --n10: #ff4fa3;  /* pink     */

  /* Cold glass. Not pure grey — a real unlit tube keeps a trace of its phosphor,
   * and a pure #808080 reads dead (see refactoring-ui §4, "saturate your greys").
   * The 15% mix is the ceiling that keeps every stop above 4.5:1; §1a checks it. */
  --neon-glass-base: #93979d;
  --neon-glass-mix: 15%;

  /* How much white is in the hot core. Below ~20% the tube reads as flat coloured
   * text rather than something emitting; above ~35% the hue disappears entirely
   * and every sign looks white. */
  --neon-core-mix: 26%;

  /* Lab intensity multiplier, 0–1.6. Shipping default is 1. */
  --neon-bloom: 1;

  /* Warm white, for colour rule B. Neon "white" is never #fff — it is argon in a
   * phosphor tube, and it sits warm. */
  --neon-white: #ffe9c9;

  /* ---- the tube face --------------------------------------------------------
   * The letterform is the whole illusion, and it is where the first two passes of
   * this lab failed. Pass one glowed Space Grotesk, which is glowing INK: stroke
   * contrast and flat-cut stems survive any amount of halo. Pass two reached for a
   * display marquee face, which reads as tubing but is chunky by construction and
   * offers no way to thin it.
   *
   * What a glass tube actually is: ONE DIAMETER end to end, ROUND terminals,
   * BENDS instead of joints. That is the definition of a monoline rounded face —
   * and it means the right knob is the WEIGHT AXIS, because in a monoline face
   * weight is not emphasis, it is literally how fat the stroke is. So gauge here
   * is a continuous 300–700 dial, not a set of classes.
   *
   * It also means the tube should be SOLID, not outlined. A lit tube seen head-on
   * is a bright line with a halo; hollowing it draws both walls of the glass and
   * reads as outline type. Hollow stays available for the thickest gauges, where
   * the bore is wide enough to actually see down. */
  --neon-face-tube: "Quicksand", "Avenir Next", system-ui, sans-serif;
  --neon-face-round: "Comfortaa", "Quicksand", system-ui, sans-serif;
  --neon-face-marquee: "Monoton", "Quicksand", system-ui, sans-serif;
  --neon-face-script: "Sacramento", "Snell Roundhand", cursive;
  --neon-face-ui: var(--font-ui);
  --neon-face: var(--neon-face-tube);

  /* Tube bore. 300 is capillary tubing, 700 is a shopfront run; 500 is the
   * standard 12mm most signs are actually bent from. */
  --tube-weight: 500;

  /* Wall thickness, used only in hollow mode. */
  --tube-gauge: .045em;
}

/* ---------------------------------------------------------------------------
 * §2 — THE TUBE PRIMITIVE
 *
 * One element, two pseudo-elements, no wrapper divs:
 *   the element itself  = the glass, and the real text a screen reader reads
 *   ::before            = the tight envelope, the tube's own halo
 *   ::after             = the room glow, the part that lands on the wall
 *
 * Both pseudos duplicate the text through attr(data-neon). If data-neon is
 * absent, content resolves empty and the sign renders as plain text — which is
 * the correct degradation, not a bug.
 * ------------------------------------------------------------------------- */
.neon {
  --hue: var(--n5);
  --glass: color-mix(in oklab, var(--hue) var(--neon-glass-mix), var(--neon-glass-base));
  --core: color-mix(in oklab, var(--hue) var(--neon-core-mix), #ffffff);

  /* The colour crossfade IS the state change. Because --lit is registered, this
   * interpolates rather than snapping, so one property drives glass -> core, the
   * tube wall and both bloom layers together, and they cannot disagree. */
  --tube-ink: color-mix(in oklab, var(--core) calc(var(--lit) * 100%), var(--glass));

  position: relative;
  display: inline-block;
  isolation: isolate;

  font-family: var(--neon-face);
  font-weight: var(--tube-weight);   /* the bore, not emphasis — see §1 */
  color: var(--tube-ink);
  transition: color 260ms cubic-bezier(.2, 0, 0, 1),
              -webkit-text-stroke-color 260ms cubic-bezier(.2, 0, 0, 1);
}

.neon::before,
.neon::after {
  content: attr(data-neon);
  position: absolute;
  inset: 0;
  z-index: -1;
  color: var(--hue);
  pointer-events: none;
  /* Emission is multiplicative: state (--lit) x lab intensity (--neon-bloom) x
   * this layer's share (--a). Setting --lit to 0 puts every layer at exactly 0,
   * so an unlit sign emits nothing at all rather than a faint residue. */
  opacity: calc(var(--lit) * var(--neon-bloom) * var(--a));
  transition: opacity 260ms cubic-bezier(.2, 0, 0, 1);
}

/* Blur in em, so a 12px badge and a 72px wall sign have the same envelope
 * proportionally. Blur radius is declared here once and is never animated — see
 * the header note; animating a filter is what makes the CodePen version stutter. */
.neon::before { --a: .9;  filter: blur(var(--glow-tight, .055em)); }
.neon::after  { --a: .55; filter: blur(var(--glow-wide, .34em)); }

/* ---------------------------------------------------------------------------
 * §2a — HAND-BENT
 *
 * The third thing that was wrong. Correct letterforms at a correct bore still
 * read as type, because type is UNIFORM and a neon sign is not: every character
 * is a separate length of glass, heated and bent by a person against a paper
 * pattern. No two are at quite the same angle, none sit at exactly the same
 * height, and the burn is never even along a run — gas fill and phosphor wear
 * differ tube to tube, so one letter in a word is always a little tired.
 *
 * So each character gets its own tube: its own rotation, its own baseline, its
 * own brightness, and its own place in the mains cycle. The offsets are
 * DETERMINISTIC, hashed from the character and its position — a sign must not
 * re-bend itself on every repaint, and the same word must look the same on two
 * machines. See splitSign() in neon-lab.js.
 *
 * --hand scales the whole effect: 0 is the machine-set version above, 1 is a
 * competent sign shop, past ~1.4 it reads as damage.
 * ------------------------------------------------------------------------- */
.neon-ch {
  /* Per-character, written by JS: -1..1 lean, -1..1 sit, 0..1 wear. */
  --lean: 0;
  --sit: 0;
  --wear: 0;

  /* Brightness of THIS length of glass. Multiplied into the parent's state so a
   * tired tube is still unambiguously off when the sign is off. */
  --litc: calc(var(--lit) * (1 - var(--wear) * .34 * var(--hand, 1)));

  display: inline-block;
  /* transform only — the governor's rule for anything that moves, and it also
   * means the jitter costs nothing to paint. */
  transform:
    translateY(calc(var(--sit) * .035em * var(--hand, 1)))
    rotate(calc(var(--lean) * 1.5deg * var(--hand, 1)));
  color: color-mix(in oklab, var(--core) calc(var(--litc) * 100%), var(--glass));
  transition: color 260ms cubic-bezier(.2, 0, 0, 1),
              -webkit-text-stroke-color 260ms cubic-bezier(.2, 0, 0, 1);
}

.neon-ch::before,
.neon-ch::after {
  content: attr(data-c);
  position: absolute;
  inset: 0;
  z-index: -1;
  color: var(--hue);
  pointer-events: none;
  opacity: calc(var(--litc) * var(--neon-bloom) * var(--a));
  transition: opacity 260ms cubic-bezier(.2, 0, 0, 1);
}
.neon-ch::before { --a: .9;  filter: blur(var(--glow-tight, .055em)); }
.neon-ch::after  { --a: .55; filter: blur(var(--glow-wide, .34em)); }
/* Positioned so the absolutely-placed bloom has a box to fill. */
.neon-ch { position: relative; }

/* A space is not a tube. Left as a styled span it collapses in inline-block. */
.neon-sp { display: inline-block; white-space: pre; }

/* Once split, the parent's own bloom would be a second, perfectly uniform copy
 * sitting behind the hand-bent one — the exact uniformity being designed out. */
.neon.is-split::before,
.neon.is-split::after { content: none; }

/* The real text, for assistive tech. The visible tubes are aria-hidden: a row of
 * inline-block single characters is read letter-by-letter by some AT/browser
 * pairs, so the split version is decorative and this carries the word. */
.neon-sr {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------------------
 * §3 — STATES
 * off (default) · on · warming · dying · out
 * ------------------------------------------------------------------------- */

/* ON. The whole state model is one number. */
.neon.is-on { --lit: 1; }

/* OUT — a dead segment inside an otherwise working sign. Not a dimmer setting:
 * the tube is broken, so it emits nothing and the glass goes slightly colder than
 * its neighbours. Used by splitting a word into per-segment spans, which is also
 * how a real sign fails (one tube in the run, not one letterform). */
.neon.is-out {
  --lit: 0;
  --glass: color-mix(in oklab, var(--hue) 6%, var(--neon-glass-base));
}

/* HOT — the momentary over-drive when a sign first strikes. Applied by JS for
 * ~420ms and removed; the transition on --lit carries the fall-off. */
.neon.is-hot { --lit: 1.32; }

/* ---------------------------------------------------------------------------
 * §4 — FACE, GAUGE AND SCALE
 * ------------------------------------------------------------------------- */

/* Face, set on any ancestor. Tracking is per-face, not global: real signs are
 * spaced to the bend radius of the tube, and a face bent from wider tube needs
 * more air or the counters close up. */
[data-face="tube"]    { --neon-face: var(--neon-face-tube); }
[data-face="round"]   { --neon-face: var(--neon-face-round); }
[data-face="marquee"] { --neon-face: var(--neon-face-marquee); --tube-gauge: .035em; }
[data-face="script"]  { --neon-face: var(--neon-face-script); }
[data-face="ui"]      { --neon-face: var(--neon-face-ui); }

[data-face="tube"] .neon,
[data-face="round"] .neon { letter-spacing: .045em; }

/* Monoton is a single-weight display face: the weight axis does nothing, so the
 * gauge dial has no effect here and the face is as thick as it is. Worth keeping
 * only as the "purpose-built marquee" reference point. */
[data-face="marquee"] .neon { letter-spacing: .1em; font-weight: 400; }

/* Script is cursive: the letters JOIN. Tracking it pulls the joins apart, which
 * destroys the one property that makes it read as a single continuous bend, and
 * uppercasing it produces a row of unconnected capitals. */
[data-face="script"] .neon { letter-spacing: 0; text-transform: none; font-weight: 400; }
[data-face="script"] .neon--sm { letter-spacing: 0; text-transform: none; font-size: 20px; }

/* Gauge presets. These set the same variable the lab's dial writes, so a
 * specimen can pin a bore while the rest of the page follows the slider. */
.neon--hair   { --tube-weight: 350; letter-spacing: .1em; }
.neon--tube   { --tube-weight: 500; }
.neon--double { --tube-weight: 700; }

.neon--sm {
  font-size: 13.5px; letter-spacing: .16em; text-transform: uppercase;
  --glow-tight: .035em; --glow-wide: .16em;
}
.neon--md { font-size: 21px; }
.neon--lg { font-size: 40px; }
.neon--xl { font-size: clamp(44px, 8vw, 86px); }

/* Room for the bloom: a tight line box slices the halo off the ascenders. */
.neon--lg, .neon--xl { line-height: 1.22; }

/* ---------------------------------------------------------------------------
 * §4a — HOLLOW GLASS
 *
 * The other half of "make it look like a tube": a lit tube is a bright WALL
 * around a dark bore, not a solid slug of colour. Filling the glyph and glowing
 * it is what makes the effect read as ink. Emptying it and stroking the contour
 * gives an actual pipe.
 *
 * The envelope layer (::before) has to be stroked too. Left solid, a filled
 * glyph sits inside the hollow one and shows through every counter — the O fills
 * in, and the illusion dies at exactly the letter that most needs to read as
 * bent glass. The room glow (::after) stays solid: at .34em of blur the fill is
 * unrecoverable anyway, and a stroked version just makes it dimmer.
 * ------------------------------------------------------------------------- */
/* Accepted on the sign itself or on any ancestor, so the lab can flip the whole
 * page at once and an individual specimen can still opt in. */
.neon[data-glass="hollow"],
[data-glass="hollow"] .neon,
[data-glass="hollow"] .neon-ch {
  color: transparent;
  -webkit-text-stroke: var(--tube-gauge) var(--tube-ink);
}
.neon[data-glass="hollow"]::before,
[data-glass="hollow"] .neon::before,
[data-glass="hollow"] .neon-ch::before {
  color: transparent;
  -webkit-text-stroke: var(--tube-gauge) var(--hue);
}
/* Each character mixes its own wall colour, so a worn tube's glass is dimmer too
 * and not just its glow. Without this every hollow sign has a uniform bright
 * outline with uneven fill, which is worse than no variation at all. */
[data-glass="hollow"] .neon-ch {
  -webkit-text-stroke-color: color-mix(in oklab, var(--core) calc(var(--litc) * 100%), var(--glass));
}

/* ---------------------------------------------------------------------------
 * §5 — COLOUR RULES
 * Three competing answers to "neon is coloured, the brand is not". Set on an
 * ancestor as data-rule so the same markup can be compared under each.
 * ------------------------------------------------------------------------- */

/* A — one stop per sign. Each sign commits to a single spectrum colour. */
[data-rule="one"] .neon { --hue: var(--sign-hue, var(--n5)); }

/* B — warm-white tubes; a spectrum colour fires only for what is genuinely live.
 * This is the rule that agrees with the console's existing "colour is reserved
 * for interaction and attention" line, because here colour IS the information. */
[data-rule="white"] .neon { --hue: var(--neon-white); }
[data-rule="white"] .neon[data-live] { --hue: var(--sign-hue, var(--n5)); }

/* C — full rainbow bends: every segment a different stop. Kept so the comparison
 * is honest and not a straw man, but note it trips three separate items on the
 * refactoring-ui NEVER list at once (3+ competing saturated accents, colour used
 * decoratively, halo everywhere) and it destroys the off-state signal, because a
 * board where everything is lit cannot say which thing is live. */
[data-rule="rainbow"] .neon:nth-child(10n + 1) { --hue: var(--n1); }
[data-rule="rainbow"] .neon:nth-child(10n + 2) { --hue: var(--n2); }
[data-rule="rainbow"] .neon:nth-child(10n + 3) { --hue: var(--n3); }
[data-rule="rainbow"] .neon:nth-child(10n + 4) { --hue: var(--n4); }
[data-rule="rainbow"] .neon:nth-child(10n + 5) { --hue: var(--n5); }
[data-rule="rainbow"] .neon:nth-child(10n + 6) { --hue: var(--n6); }
[data-rule="rainbow"] .neon:nth-child(10n + 7) { --hue: var(--n7); }
[data-rule="rainbow"] .neon:nth-child(10n + 8) { --hue: var(--n8); }
[data-rule="rainbow"] .neon:nth-child(10n + 9) { --hue: var(--n9); }
[data-rule="rainbow"] .neon:nth-child(10n)     { --hue: var(--n10); }

/* ---------------------------------------------------------------------------
 * §6 — THE MOUNT
 * The wall, the wash it throws, the pins that hold the tube off the surface.
 * These are what separate "glowing text" from "an object on a wall".
 * ------------------------------------------------------------------------- */
.neon-mount {
  position: relative;
  isolation: isolate;
  padding: clamp(22px, 5vw, 54px) clamp(18px, 4vw, 44px);
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--border);
  overflow: hidden;
}

/* Plaster. A real wall is never flat, and a perfectly flat field is what makes a
 * CSS neon look like CSS. Two offset dot-grids at low alpha read as tooth
 * without turning into a visible pattern. */
.neon-mount[data-wall="plaster"] {
  background-image:
    radial-gradient(rgba(255, 255, 255, .022) 1px, transparent 1px),
    radial-gradient(rgba(0, 0, 0, .3) 1px, transparent 1px);
  background-size: 7px 7px, 11px 11px;
  background-position: 0 0, 3px 4px;
}

/* The wash. Sized in % of the mount rather than em of the type, because the
 * spill is a property of the room, not of the letterforms. */
.neon-wash {
  position: absolute;
  inset: -30%;
  z-index: -2;
  pointer-events: none;
  background: radial-gradient(closest-side ellipse at 50% 45%, var(--hue, var(--n5)), transparent 72%);
  opacity: calc(var(--lit) * var(--neon-bloom) * .16);
  transition: opacity 420ms cubic-bezier(.2, 0, 0, 1);
}

/* Mounting pins — the standoffs that hold a tube ~25mm off the wall. Two per
 * segment, unlit always, because the hardware is not part of the circuit. */
.neon-pin {
  position: absolute;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: #55585d;
  box-shadow: 0 1px 0 rgba(0, 0, 0, .6);
}

/* The transformer box. Every real sign has one and it is always slightly ugly. */
.neon-box {
  position: absolute;
  right: 14px;
  bottom: 12px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  border-radius: 4px;
  background: #16171a;
  border: 1px solid var(--border);
  font-family: var(--font-mono-ui);
  font-size: 9.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--faint);
}
.neon-box i {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: color-mix(in oklab, var(--n5) calc(var(--lit) * 100%), #3a3d41);
  transition: background 260ms linear;
}

/* ---------------------------------------------------------------------------
 * §7 — APPLIED
 * The primitive meeting real console components. This is the part that decides
 * whether the idea survives; a specimen strip always looks good.
 * ------------------------------------------------------------------------- */

/* 7a — the LIVE marquee. The sign over the door. */
.neon-live {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 11px 5px 9px;
  border-radius: 999px;
  border: 1px solid color-mix(in oklab, var(--hue) calc(var(--lit) * 42%), var(--border));
  background: color-mix(in oklab, var(--hue) calc(var(--lit) * 7%), transparent);
  font-family: var(--font-mono-ui);
  transition: border-color 260ms ease, background-color 260ms ease;
}
.neon-live .dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex: none;
  background: color-mix(in oklab, var(--core) calc(var(--lit) * 100%), var(--glass));
  box-shadow: 0 0 0 0 transparent;
}

/* 7b — stat figures. The whole argument in one component: a zero is UNLIT GLASS,
 * and the first photo landing strikes the tube. The dashboard currently draws a
 * grey 0 that looks identical to a broken query. */
.neon-stat {
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 15px 16px 17px;
  border-radius: var(--r-card);
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--ring-top);
}
.neon-stat .k {
  font-family: var(--font-mono-ui);
  font-size: 10.5px;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--faint);
}
/* No font-weight here on purpose: the tube faces ship one weight, and asking for
 * a heavier one gets a synthesised bold that fattens the tube unevenly. */
.neon-stat .neon {
  font-size: 34px;
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
}

/* 7c — controls. A button is not a sign, so it does not get a permanent tube;
 * it borrows the WARM-UP, which is what a real sign does in the second before it
 * settles. Hover flickers, focus holds steady — the keyboard path is never the
 * flickering one, because a flicker under a stationary caret reads as a fault. */
.neon-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 15px;
  border-radius: var(--r-ctl);
  background: transparent;
  border: 1px solid color-mix(in oklab, var(--hue) calc(var(--lit) * 55%), var(--border-2));
  color: color-mix(in oklab, var(--core) calc(var(--lit) * 100%), #d7dade);
  font-family: var(--font-ui);
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 200ms ease, color 200ms ease;
}
.neon-btn::after {
  content: "";
  position: absolute;
  inset: -1px;
  z-index: -1;
  border-radius: inherit;
  background: var(--hue);
  filter: blur(9px);
  opacity: calc(var(--lit) * var(--neon-bloom) * .3);
  transition: opacity 200ms ease;
  pointer-events: none;
}
.neon-btn:focus-visible { --lit: 1; outline: 2px solid transparent; outline-offset: 2px; }
.neon-btn:active { transform: translateY(1px); }

/* 7d — nav rail. The active item's indicator is a lit segment; everything else is
 * cold glass. One lit item per list, enforced by there only being one active row. */
.neon-nav { display: flex; flex-direction: column; gap: 2px; }
.neon-nav a {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  border-radius: 7px;
  color: var(--muted);
  text-decoration: none;
  font-size: 13.5px;
  font-weight: 600;
}
.neon-nav a::before {
  content: "";
  position: absolute;
  left: 0;
  top: 8px;
  bottom: 8px;
  width: 2px;
  border-radius: 2px;
  background: color-mix(in oklab, var(--hue) calc(var(--lit) * 100%), #3a3d41);
  transition: background 220ms ease;
}
.neon-nav a[aria-current] {
  --lit: 1;
  color: var(--text);
  background: var(--surface-2);
}
.neon-nav a[aria-current]::after {
  content: "";
  position: absolute;
  left: -2px;
  top: 8px;
  bottom: 8px;
  width: 6px;
  z-index: -1;
  background: var(--hue);
  filter: blur(7px);
  opacity: calc(var(--neon-bloom) * .55);
}
.neon-nav a:hover:not([aria-current]) { color: var(--text); background: var(--surface); }

/* 7e — the empty state. The single best fit for the whole idea: a board with
 * nothing on it is not an error, it is a sign that has not been switched on. */
.neon-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  text-align: center;
  padding: clamp(34px, 7vw, 66px) 24px;
}
.neon-empty p {
  max-width: 46ch;
  margin: 0;
  color: var(--muted);
  font-size: 14px;
  line-height: 1.6;
}

/* ---------------------------------------------------------------------------
 * §7f — THE SHIPPING SIDEBAR
 *
 * The first piece of this that is not a specimen. nav-app.html wraps each nav
 * label and the brand wordmark in .neon; everything below turns the sidebar into
 * a board of signs.
 *
 * The model: no chrome at all. A neon sign has no card behind it and no pill
 * around it — it is glass on a wall — so the row loses its background, its
 * border and its accent rail, and the ONLY thing that distinguishes states is
 * whether the tube is burning. Take the light away and there is nothing left to
 * look at, which is the point: nothing here is decoration.
 *
 * State: dark by default, lit on hover, lit where you are. Hover lighting means
 * a second tube burns while the pointer is in the sidebar — a deliberate trade,
 * because in a MENU the hover light reads as "this is the one you're about to
 * switch on" rather than as a competing status claim.
 *
 * Scoped under [data-theme="host"] so the light checkout/login pages, which use
 * the same nav partial, never pick any of it up.
 * ------------------------------------------------------------------------- */

/* Every tube variable lives on the ROW, not on the label, so the icon and the
 * lettering are visibly one circuit fed by one transformer. */
html[data-theme="host"] .app-nav-item {
  --hue: var(--n7);
  --glass: color-mix(in oklab, var(--hue) var(--neon-glass-mix), var(--neon-glass-base));
  --core: color-mix(in oklab, var(--hue) var(--neon-core-mix), #ffffff);
  --tube-ink: color-mix(in oklab, var(--core) calc(var(--lit) * 100%), var(--glass));

  background: none;
  border-color: transparent;
  color: var(--tube-ink);
  gap: 11px;
  padding: 8px 10px;
  transition: color 220ms cubic-bezier(.2, 0, 0, 1);
}

/* No pill, no card, no rail — including in the states that used to draw them. */
html[data-theme="host"] .app-nav-item:hover,
html[data-theme="host"] .app-nav-item.active {
  background: none;
  border-color: transparent;
}
html[data-theme="host"] .app-nav-item.active::before { content: none; }

/* Lit where you are, and lit under the pointer. */
html[data-theme="host"] .app-nav-item.active { --lit: 1; }
@media (hover: hover) and (pointer: fine) {
  html[data-theme="host"] .app-nav-item:hover { --lit: 1; }
}
/* Keyboard gets the same light — a focused row you cannot see is worse than no
 * effect at all — plus a real focus ring, because a glow is not a focus
 * indicator and cannot be relied on in forced-colours or by anyone who cannot
 * distinguish the two brightness states. */
html[data-theme="host"] .app-nav-item:focus-visible {
  --lit: 1;
  outline: 2px solid var(--hue);
  outline-offset: 2px;
}

html[data-theme="host"] .app-nav-item .neon {
  --hue: inherit;
  --tube-weight: 500;
  font-size: 15.5px;
  letter-spacing: .04em;
  --glow-tight: .04em;
  --glow-wide: .2em;
}

/* The icons ARE tubes already: every one is a stroked path with round caps and
 * round joins, which is the exact geometry of bent glass. They need no redraw —
 * only a bore and an emission.
 *
 * The glow is a drop-shadow whose COLOUR carries the state: at --lit 0 it mixes
 * to transparent, so the filter string never changes and nothing is animated
 * except paint. Animating the filter itself (a changing blur radius) is the
 * expensive version and is exactly what §8 forbids. */
html[data-theme="host"] .app-nav-item svg {
  width: 18px;
  height: 18px;
  stroke: var(--tube-ink);
  stroke-width: 1.9;
  filter:
    drop-shadow(0 0 2px color-mix(in oklab, var(--hue) calc(var(--lit) * 90%), transparent))
    drop-shadow(0 0 7px color-mix(in oklab, var(--hue) calc(var(--lit) * 55%), transparent));
  transition: stroke 220ms cubic-bezier(.2, 0, 0, 1);
}

/* One colour per tab. Deliberately NOT sequential down the spectrum: adjacent
 * stops are ~30 degrees apart and a run of them reads as a gradient, which is
 * the thing the brand rules out. These are picked to jump the wheel so no two
 * neighbours are confusable — azure, pink, amber, green, violet, orange, teal. */
html[data-theme="host"] .app-nav-item:nth-of-type(1) { --hue: var(--n7); }
html[data-theme="host"] .app-nav-item:nth-of-type(2) { --hue: var(--n10); }
html[data-theme="host"] .app-nav-item:nth-of-type(3) { --hue: var(--n3); }
html[data-theme="host"] .app-nav-item:nth-of-type(4) { --hue: var(--n5); }
html[data-theme="host"] .app-nav-item:nth-of-type(5) { --hue: var(--n9); }
html[data-theme="host"] .app-nav-item:nth-of-type(6) { --hue: var(--n2); }
html[data-theme="host"] .app-nav-item:nth-of-type(7) { --hue: var(--n6); }

/* THE BRAND WORDMARK IS DELIBERATELY NOT A TUBE.
 *
 * It was, and it shipped clipped. `.app-brand` is 211px wide, the TAPSNAP lockup
 * takes 116px of that, and "PHOTOPOOL" as a tube needs 154px in the 69px that is
 * left — `.app-brand span` has overflow:hidden, so it silently rendered as half a
 * word. Quicksand at a readable bore is simply wider than the 11px uppercase
 * Space Grotesk it replaced, and no amount of tightening closes an 85px gap.
 *
 * Shrinking it far enough to fit puts the type near 10px, where the stroke is
 * about 1px and the halo swamps it — the failure mode documented on the brand
 * rule above. So the wordmark keeps its original type. The sidebar's LETTERING is
 * the nav, and that is where the tubes are.
 *
 * If the brand should be a sign, the fix is to give it the row: drop the TAPSNAP
 * lockup from the sidebar and let PHOTOPOOL have all 211px at ~17px. That is a
 * branding call, not a CSS one. */

/* ---------------------------------------------------------------------------
 * §8 — MOTION
 *
 * Every loop below animates OPACITY and nothing else, on elements that are
 * already their own layer. No filter, no shadow, no colour is animated in a
 * keyframe — those are the properties that make this effect janky, and they are
 * confined to one-shot transitions in §2 and §7.
 * ------------------------------------------------------------------------- */

/* Mains buzz. A real tube ripples at the supply frequency; the amplitude here is
 * ~4%, which is under the threshold where it reads as "animation" and over the
 * threshold where its absence reads as a flat graphic. Opt-in per sign. */
@keyframes neon-buzz {
  0%, 100% { opacity: calc(var(--lit) * var(--neon-bloom) * var(--a)); }
  46%      { opacity: calc(var(--lit) * var(--neon-bloom) * var(--a) * .955); }
  72%      { opacity: calc(var(--lit) * var(--neon-bloom) * var(--a) * 1.03); }
}

/* Warm-up. Deliberately irregular — an evenly-spaced flicker reads as a strobe.
 * The long dead beat at 34–43% is the part that sells it. */
@keyframes neon-strike {
  0%, 12%  { opacity: 0; }
  14%      { opacity: calc(var(--lit) * var(--neon-bloom) * var(--a) * .9); }
  16%, 33% { opacity: 0; }
  35%      { opacity: calc(var(--lit) * var(--neon-bloom) * var(--a) * .55); }
  43%      { opacity: 0; }
  46%      { opacity: calc(var(--lit) * var(--neon-bloom) * var(--a)); }
  52%      { opacity: calc(var(--lit) * var(--neon-bloom) * var(--a) * .2); }
  58%, 100% { opacity: calc(var(--lit) * var(--neon-bloom) * var(--a)); }
}

/* A single length of glass that doesn't quite hold.
 *
 * Applied to the WHOLE character rather than to its glow layers, because that is
 * what a stutter physically is: the tube stops emitting, so the glass and the
 * light it throws go together. A pseudo-element inherits its originating
 * element's opacity, so animating one property here dims the letter, its
 * envelope and its room glow in lockstep — one compositor-driven property for
 * all three layers, and no way for them to disagree.
 *
 * The shape is ~97% ON. A tube that flickers steadily is a strobe and reads as
 * broken; a tube that drops out twice in eight seconds reads as ALIVE. The
 * dropouts are clustered rather than evenly spaced for the same reason the
 * warm-up is irregular — evenly spaced anything reads as a machine. */
@keyframes neon-stutter {
  0%, 95.8% { opacity: 1; }
  96.1%     { opacity: .26; }
  96.5%     { opacity: 1; }
  97.2%     { opacity: .2; }
  97.6%     { opacity: .86; }
  97.9%     { opacity: .3; }
  98.4%, 100% { opacity: 1; }
}

/* A failing tube: mostly on, with a brief stutter every few seconds. */
@keyframes neon-fault {
  0%, 88%, 100% { opacity: calc(var(--lit) * var(--neon-bloom) * var(--a)); }
  89%, 91%      { opacity: calc(var(--lit) * var(--neon-bloom) * var(--a) * .12); }
  90%           { opacity: calc(var(--lit) * var(--neon-bloom) * var(--a) * .8); }
}

/* prefers-reduced-motion is applied as the ENABLING condition, never as an
 * override to switch off later. If the query is unsupported or the stylesheet is
 * partially applied, the failure mode is a still sign rather than a strobing one.
 * Same discipline as seam.css §15. */
@media (prefers-reduced-motion: no-preference) {
  .neon[data-buzz]::before,
  .neon[data-buzz]::after,
  .neon[data-buzz] .neon-ch::before,
  .neon[data-buzz] .neon-ch::after {
    animation: neon-buzz 2.6s ease-in-out infinite;
  }
  .neon[data-buzz]::after,
  .neon[data-buzz] .neon-ch::after { animation-duration: 3.9s; }

  /* Negative delay starts each tube part-way into the cycle. Without it every
   * character breathes in lockstep, which is a single flickering graphic rather
   * than nine separate lengths of glass on one transformer. */
  .neon[data-buzz] .neon-ch::before,
  .neon[data-buzz] .neon-ch::after {
    animation-delay: calc(var(--phase, 0) * -2.6s);
  }

  .neon.is-striking::before,
  .neon.is-striking::after,
  .neon.is-striking .neon-ch::before,
  .neon.is-striking .neon-ch::after {
    animation: neon-strike 1.15s steps(1, end) 1;
  }
  /* Striking is staggered far less than buzz — a sign comes up as one circuit,
   * just not perfectly together. */
  .neon.is-striking .neon-ch::before,
  .neon.is-striking .neon-ch::after {
    animation-delay: calc(var(--phase, 0) * 90ms);
  }

  /* Which characters stutter is decided in js/host/neon.js — roughly half of all
   * signs get one, and it is the most worn tube in the word, because the tube
   * that is nearly gone is the one that should be struggling.
   *
   * Only ever runs on a LIT sign. An unlit tube cannot flicker; it is already
   * off, and dimming cold glass would read as the text fading rather than as a
   * light failing.
   *
   * Every duration is different (7s to 12.6s, keyed off the character's own
   * phase) and every start is offset. Identical periods would drift into sync
   * and the whole sidebar would blink together, which is the single thing that
   * would give the effect away as CSS. */
  .neon.is-on .neon-ch[data-flicker],
  html[data-theme="host"] .app-nav-item.active .neon-ch[data-flicker] {
    animation: neon-stutter calc(7s + var(--phase, 0) * 5.6s) steps(1, end) infinite;
    animation-delay: calc(var(--phase, 0) * -9s);
  }

  .neon.is-fault::before,
  .neon.is-fault::after,
  .neon.is-fault .neon-ch::before,
  .neon.is-fault .neon-ch::after {
    animation: neon-fault 5.2s steps(1, end) infinite;
  }
  /* The fault is ONE bad section, not the whole word stuttering in unison. */
  .neon.is-fault .neon-ch::before,
  .neon.is-fault .neon-ch::after {
    animation-delay: calc(var(--phase, 0) * -5.2s);
  }

  /* Hover borrows the warm-up rather than the burn — 480ms, one pass. Pointer
   * only: :hover fires on tap on touch devices and would leave a control
   * mid-flicker after the finger lifts. */
  @media (hover: hover) and (pointer: fine) {
    .neon-btn:hover { --lit: 1; }
    .neon-btn:hover::after { animation: neon-strike 480ms steps(1, end) 1; }
  }
}

/* Without the pointer query above, hover still has to light the control — it just
 * does it without the flicker. State is never dependent on motion being allowed. */
@media (hover: hover) and (pointer: fine) {
  .neon-btn:hover { --lit: 1; }
}

/* will-change only where something is actually about to animate, and only while
 * it is. A board of twenty permanently-promoted blurred layers is how this
 * effect ends up costing more than it earns. */
.neon[data-buzz] .neon-ch::before,
.neon[data-buzz] .neon-ch::after,
.neon.is-striking .neon-ch::before,
.neon.is-striking .neon-ch::after,
.neon[data-buzz]::before,
.neon[data-buzz]::after,
.neon.is-striking::before,
.neon.is-striking::after { will-change: opacity; }

/* ---------------------------------------------------------------------------
 * §9 — FALLBACKS
 * ------------------------------------------------------------------------- */

/* Forced colours: the OS is picking, so emission is meaningless and blurred
 * duplicates behind text actively hurt legibility. Strip to plain text and let
 * the state be carried by the text itself. */
@media (forced-colors: active) {
  .neon::before,
  .neon::after,
  .neon-wash,
  .neon-btn::after,
  .neon-nav a[aria-current]::after { display: none; }
  .neon { color: CanvasText; }
  .neon.is-on { color: LinkText; }
}

/* Print: a glow costs ink and prints as a grey smear. */
@media print {
  .neon::before,
  .neon::after,
  .neon-wash,
  .neon-btn::after { display: none !important; }
  .neon { color: #000 !important; }
}

/* ---------------------------------------------------------------------------
 * §10 — LAB CHROME
 * Only the /lab/neon page uses anything below. None of it is part of the
 * proposal; it exists to make the proposal comparable.
 * ------------------------------------------------------------------------- */
.nl-wrap { max-width: 1080px; margin: 0 auto; padding: 26px clamp(16px, 3vw, 32px) 120px; }

.nl-head { border-bottom: 1px solid var(--border); padding-bottom: 20px; margin-bottom: 26px; }
.nl-head h1 { font-size: 26px; font-weight: 700; margin: 6px 0 8px; letter-spacing: -.01em; }
.nl-eyebrow {
  font-family: var(--font-mono-ui);
  font-size: 10.5px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0;
}
.nl-lede { max-width: 68ch; color: var(--muted); font-size: 15px; line-height: 1.65; margin: 0; }

.nl-sec { margin: 46px 0 0; }
.nl-sec > h2 {
  font-size: 12px;
  font-family: var(--font-mono-ui);
  letter-spacing: .15em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0 0 6px;
  font-weight: 400;
}
.nl-sec > .nl-note { max-width: 68ch; color: var(--muted); font-size: 13.5px; line-height: 1.65; margin: 0 0 20px; }
.nl-note b { color: var(--text); font-weight: 600; }

/* Sticky control bar. */
.nl-bar {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 18px;
  padding: 11px clamp(16px, 3vw, 32px);
  margin-bottom: 26px;
  background: rgba(15, 16, 17, .93);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border);
}
.nl-grp { display: flex; align-items: center; gap: 7px; }
.nl-grp > span {
  font-family: var(--font-mono-ui);
  font-size: 9.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--faint);
}
.nl-seg { display: inline-flex; border: 1px solid var(--border-2); border-radius: 7px; overflow: hidden; }
.nl-seg button {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 600;
  padding: 6px 11px;
  cursor: pointer;
}
.nl-seg button + button { border-left: 1px solid var(--border); }
.nl-seg button[aria-pressed="true"] { background: var(--surface-3); color: var(--text); }
.nl-seg button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.nl-bar input[type="range"] { width: 108px; accent-color: var(--accent); }
.nl-bar output { font-family: var(--font-mono-ui); font-size: 11px; color: var(--muted); min-width: 2.4em; }

/* Specimen grids. */
.nl-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
.nl-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; }
/* Always exactly three items, so the column count is stated rather than fitted.
 * auto-fit with minmax(260px, 1fr) resolves to two columns at 820px and four at
 * 1280px, and both leave a ragged last row — the same trap that hit the cloth,
 * cast and lab grids on the marketing side. Three or one, nothing between. */
.nl-3 { display: grid; grid-template-columns: 1fr; gap: 14px; }
@media (min-width: 760px) { .nl-3 { grid-template-columns: repeat(3, 1fr); } }

/* min-width:0 because grid and flex children default to min-width:auto, which
 * refuses to shrink below their content. Monoton is roughly twice the set width
 * of Space Grotesk, so without this a long sign forces its track wider than the
 * page and the last column is pushed off-screen. */
.nl-cell {
  display: flex;
  flex-direction: column;
  min-width: 0;
  gap: 12px;
  padding: 20px 18px;
  border-radius: var(--r-card);
  background: var(--surface);
  border: 1px solid var(--border);
}
.nl-cell > .cap {
  font-family: var(--font-mono-ui);
  font-size: 9.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--faint);
}
.nl-cell > .cap b { color: var(--muted); font-weight: 400; }
.nl-cell .body { display: flex; align-items: center; justify-content: center; min-height: 62px; }

/* The anatomy exploder: the same word with layers progressively added. */
.nl-anatomy { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
.nl-anatomy .nl-cell .body { min-height: 84px; }
.nl-strip-hide-before::before,
.nl-strip-hide-before .neon-ch::before { display: none; }
.nl-strip-hide-after::after,
.nl-strip-hide-after .neon-ch::after { display: none; }

/* Fake console frame for the applied section — enough shell to judge the
 * components in context without pulling in the real app's session machinery. */
.nl-console {
  display: grid;
  grid-template-columns: 190px 1fr;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--bg);
}
.nl-console-side { padding: 14px 10px; background: #101113; border-right: 1px solid var(--border); }
.nl-console-main { padding: 20px; display: flex; flex-direction: column; gap: 18px; }
.nl-console-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
.nl-console-head h3 { margin: 0; font-size: 17px; font-weight: 700; }
.nl-console-head .sub { font-size: 12.5px; color: var(--faint); margin-top: 2px; }

@media (max-width: 640px) {
  .nl-console { grid-template-columns: 1fr; }
  .nl-console-side { display: none; }
}

/* Master switches, lab only.
 *
 * These have to outrank `.neon.is-on` (0,2,0), which sets --lit on the element
 * itself and therefore beats any inherited value from an ancestor no matter how
 * high up it is set. `body[data-power] .neon` lands at (0,2,1) and wins by one
 * element — worth spelling out, because the obvious version of this (setting
 * --lit on <html>) silently does nothing and looks like a broken toggle. */
body[data-power="off"] .neon,
body[data-power="off"] .neon-wash,
body[data-power="off"] .neon-box i { --lit: 0; }
body[data-power="off"] .neon::before,
body[data-power="off"] .neon::after { animation: none; }

body[data-buzz="off"] .neon::before,
body[data-buzz="off"] .neon::after { animation: none; }

/* The stutter lives on the CHARACTER, not on its glow layers, so the two rules
 * above miss it. It also has to out-specify `.neon.is-on .neon-ch[data-flicker]`
 * (0,4,0) — hence the redundant-looking `.neon` in the middle, which buys the
 * extra class the override needs. Without it the buzz switch reads as broken:
 * everything else stops and one letter keeps blinking. */
body[data-power="off"] .neon .neon-ch[data-flicker],
body[data-buzz="off"] .neon .neon-ch[data-flicker] { animation: none; opacity: 1; }

/* Contrast readout table. */
.nl-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 4px; }
.nl-table th, .nl-table td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--border); }
.nl-table th { font-family: var(--font-mono-ui); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--faint); font-weight: 400; }
.nl-table td { font-variant-numeric: tabular-nums; color: var(--muted); }
.nl-table td:first-child { color: var(--text); }
.nl-scroll { overflow-x: auto; }
