/* abBookmarks — one rack-mounted device.
   Near-black chassis, a lighter machined faceplate, engraved hairlines, silkscreen
   labels in tracked caps and monospace for every number. No rounded cards: rows are
   channels on a panel, grouped by alignment and rules. */

@font-face {
    font-family: "Barlow SC";
    src: url("fonts/BarlowSemiCondensed-400.woff2") format("woff2");
    font-weight: 400; font-display: swap;
}
@font-face {
    font-family: "Barlow SC";
    src: url("fonts/BarlowSemiCondensed-500.woff2") format("woff2");
    font-weight: 500; font-display: swap;
}
@font-face {
    font-family: "Barlow SC";
    src: url("fonts/BarlowSemiCondensed-600.woff2") format("woff2");
    font-weight: 600; font-display: swap;
}
@font-face {
    font-family: "Barlow SC";
    src: url("fonts/BarlowSemiCondensed-700.woff2") format("woff2");
    font-weight: 700; font-display: swap;
}
/* Faceplate display faces. Three files, 66 KB together — less than the single
   JetBrains Mono 700 already shipping, and each is a latin subset. They earn
   their bytes under the Two Files Per Face Rule by carrying a whole theme's
   identity rather than styling one element: a comic cel is not a comic cel in a
   grotesque, and a doodle in Barlow is just a note.

   Each is TITLES ONLY. Body text, labels and readouts stay Barlow and JBMono on
   every faceplate — a display face at 0.68rem is unreadable, which is the whole
   reason the type ramp has no display tier. All three are SIL Open Font License. */
@font-face {
    font-family: "Bangers";
    src: url("fonts/Bangers-400.woff2") format("woff2");
    font-weight: 400; font-display: swap;
}
@font-face {
    font-family: "PatrickHand";
    src: url("fonts/PatrickHand-400.woff2") format("woff2");
    font-weight: 400; font-display: swap;
}
@font-face {
    font-family: "Bitter";
    src: url("fonts/Bitter-700.woff2") format("woff2");
    font-weight: 700; font-display: swap;
}

@font-face {
    font-family: "JBMono";
    src: url("fonts/JetBrainsMono-400.woff2") format("woff2");
    font-weight: 400; font-display: swap;
}

@font-face {
    font-family: "JBMono";
    src: url("fonts/JetBrainsMono-700.woff2") format("woff2");
    font-weight: 700; font-display: swap;
}

/* Fallback metrics matched to Barlow Semi Condensed, so the tracked caps do not
   reflow when the real face swaps in. */
@font-face {
    font-family: "Barlow SC Fallback";
    src: local("Segoe UI"), local("Arial");
    size-adjust: 87%;
    ascent-override: 105%;
    descent-override: 25%;
    line-gap-override: 0%;
}

:root {
    --chassis: #08090c;      /* shipped ground; the contract's #0a0c10 was the target */
    --plate: #14181f;        /* shipped plate; the contract's #161b22 was the target */
    --plate-hi: #1b212a;
    --recess: #0d1015;

    --engrave: #262d38;      /* hairline rules milled into the plate */
    --engrave-hi: #39424f;

    --ink: #dfe6ee;          /* silkscreen white */
    --ink-2: #9aa6b4;
    --ink-3: #7b8796;      /* 4.5:1 on the chassis; #6b7684 measured 4.31 */

    --signal: #ff8a1e;       /* the one live colour: amber */
    --signal-dim: rgba(255, 138, 30, .14);
    --led-on: #43d17a;
    --led-warn: #ffc94a;
    --fault: #ff5d5d;

    --sans: "Barlow SC", "Barlow SC Fallback", "Segoe UI", system-ui, sans-serif;
    --mono: "JBMono", ui-monospace, "Cascadia Mono", Consolas, monospace;

    --ease: cubic-bezier(.2, .9, .3, 1);

    /* Values that assume a dark ground; the light plate restates each one. */
    --on-signal: #14100a;                     /* ink on the amber key */
    --signal-edge: #d9741a;
    --grain: rgba(255, 255, 255, .012);       /* brushed metal striation */
    --machined: rgba(0, 0, 0, .5);            /* inset shadow inside a milled slot */
    --machined-soft: rgba(0, 0, 0, .45);
    --drop: rgba(0, 0, 0, .7);                /* the drop-down's shadow */
    --scroll-hover: #4a5665;
    --fault-ink: #ffb3b3;
    --on-fault: #1a0808;
    --thumb-filter: var(--thumb-filter);
    --thumb-read: brightness(.6) saturate(.55);
    --tile-l1: 26%;                           /* lettered-tile gradient lightness */
    --tile-l2: 11%;
    --tile-ink-l: 72%;
    --tag-mix: white;                         /* what a tag tints toward on hover */
    --tag-ink-mix: 78%;                       /* how much of the tag's colour the label keeps */
    --tag-ink-base: var(--ink-2);

    /* Shape and character, not just colour. A theme is allowed to change the form of
       the instrument, so the things that make it look milled-from-metal are tokens
       too: square corners, no glow, and the condensed grotesque doing the labels. */
    --radius: 0;                              /* milled edges: nothing is rounded */
    --radius-key: 0;                          /* except where a knob demands it */
    --glow: none;                             /* dark panel is lit, not luminous */
    --glow-key: none;
    --label-font: var(--sans);
    --title-font: var(--sans);
    --label-tracking: .17em;
    --border-width: 1px;

    /* Derived from the state colours so a theme restating --led-on/--fault/--signal
       moves its soft borders with it. These were hardcoded rgba() copies that stayed
       amber and green no matter which theme was on. */
    /* The signal colour doing two different jobs. As a KEY it is a background with
       --on-signal ink on it. As TEXT at 0.7rem it must clear 4.5:1 by itself, and on
       a light ground the amber that looks right on a key does not — measured 2.23:1
       on the light plate. Dark grounds need no separate value; light ones restate it. */
    --signal-text: var(--signal);

    --led-warn-edge: color-mix(in srgb, var(--led-warn) 30%, transparent);
    --led-on-edge: color-mix(in srgb, var(--led-on) 28%, transparent);
    --fault-edge: color-mix(in srgb, var(--fault) 40%, transparent);
    --signal-edge-soft: color-mix(in srgb, var(--signal) 40%, transparent);
    --knob-ring: rgba(255, 255, 255, .35);
}

/* A light faceplate is not the dark one inverted: it is bead-blasted aluminium with
   dark silkscreen, so the plate sits ABOVE the surrounding chassis rather than below
   it, and the milled slots stay slightly darker than the metal around them. */
:root[data-theme="light"] {
    --chassis: #d8dadd;
    --plate: #ecedef;
    --plate-hi: #f5f6f7;
    --recess: #ffffff;

    --engrave: #b9bdc3;
    --engrave-hi: #9aa0a8;

    --ink: #14181d;
    --ink-2: #464e57;
    --ink-3: #515a64;      /* 4.5:1 on the light CHASSIS, which is darker than the plate */

    --signal: #d96a00;     /* amber darkened to carry white ink and pass contrast */
    --signal-dim: rgba(217, 106, 0, .12);
    --led-on: #1c8f4a;
    --led-warn: #a8770a;
    --fault: #c0392b;

    /* White on amber measured 3.49:1. Dark ink on the same amber clears the floor
       without dulling the one colour the panel spends on its live control. */
    --on-signal: #2a1400;
    --signal-edge: #b85a00;
    --grain: rgba(0, 0, 0, .022);
    --machined: rgba(0, 0, 0, .13);
    --machined-soft: rgba(0, 0, 0, .18);
    --drop: rgba(0, 0, 0, .22);
    --scroll-hover: #8b929b;
    --fault-ink: #96271b;
    --on-fault: #ffffff;
    --thumb-filter: none;
    --thumb-read: grayscale(.55) opacity(.75);
    --tile-l1: 84%;
    --tile-l2: 72%;
    --tile-ink-l: 32%;
    --tag-mix: black;
    /* On a light plate a 78% tint of a mid-lightness hue is too pale to read, so the
       label keeps less colour and mixes toward the dark ink rather than the grey. */
    --tag-ink-mix: 62%;
    --tag-ink-base: var(--ink);

    /* Amber small text measured 2.23:1 on this plate. Darkened for the text role
       only; the key keeps the brighter --signal as its background. #9c4a00 lifted
       it to 3.96 — still short, because the switch sits on a tinted --signal-dim
       ground rather than the bare plate. This clears 4.5 on both. */
    --signal-text: #7d3a00;

    /* Same milled instrument, lit differently: the form does not change. */
    --knob-ring: rgba(0, 0, 0, .18);
}

/* ---------- cyberpunk ----------

   Not the dark panel with different paint. This is a lit sign seen through rain:
   the surface is nearly black so the colour can burn, every edge glows rather than
   being milled, and the labels are mono throughout because on this panel everything
   reads as a machine emission. Magenta is the live colour; cyan is the second voice.

   The ink is deliberately near-white rather than neon — neon text on a black ground
   at 0.7rem is a legibility disaster, so the glow lives in the borders and the
   signal, and the reading text stays calm. */
:root[data-theme="cyberpunk"] {
    --chassis: #05020a;
    --plate: #0d0618;
    --plate-hi: #150a24;
    --recess: #080310;

    --engrave: #34164d;
    --engrave-hi: #6a2a94;

    --ink: #f2e9ff;
    --ink-2: #b9a3d6;
    --ink-3: #9b86b8;

    --signal: #ff2d95;                        /* hot magenta: the one live colour */
    --signal-dim: rgba(255, 45, 149, .16);
    --led-on: #2bf5c8;                        /* cyan-green, the second voice */
    --led-warn: #ffd166;
    --fault: #ff4d6d;

    --on-signal: #12000a;
    --signal-edge: #c9186f;
    --grain: rgba(255, 45, 149, .022);
    --machined: rgba(0, 0, 0, .65);
    --machined-soft: rgba(0, 0, 0, .5);
    --drop: rgba(255, 45, 149, .28);
    --scroll-hover: #7a3aa8;
    --fault-ink: #ff9db0;
    --on-fault: #16020a;
    --thumb-filter: saturate(1.25) contrast(1.06);
    --thumb-read: brightness(.55) saturate(.7);
    --tile-l1: 30%;
    --tile-l2: 12%;
    --tile-ink-l: 76%;
    --tag-mix: white;
    --tag-ink-mix: 74%;
    --tag-ink-base: var(--ink-2);

    /* The restyle: edges glow instead of being cut, and every label is mono. */
    --radius: 0;
    --radius-key: 0;
    --glow: 0 0 0 1px rgba(255, 45, 149, .10), 0 0 12px -4px rgba(255, 45, 149, .35);
    --glow-key: 0 0 14px -2px rgba(255, 45, 149, .6);
    --label-font: var(--mono);
    /* Mono titles looked right and cost real information: measured 3 of 25 titles
       clipping the two-line clamp against 0 in every other theme. The labels and
       readouts carry the machine character; the titles are the one place here that
       has to be read, so they keep the condensed sans. */
    --title-font: var(--sans);
    --title-weight: 500;
    --title-tracking: .005em;
    --label-tracking: .2em;
    --border-width: 1px;

    /* Cards are slabs cut from the plate, edged in magenta and lit on hover.
       The rule underneath goes: these do not need one, they have edges. */
    --card-bg: #0b0416;
    --card-rule: 1px solid #2a1140;
    --card-radius: 0;
    --card-pad: .6rem .55rem;
    --card-gap: .3rem;
    --card-shadow: inset 2px 0 0 rgba(255, 45, 149, .5);
    --card-shadow-hover: inset 2px 0 0 var(--signal), 0 0 18px -6px rgba(255, 45, 149, .5);
    --thumb-radius: 0;

    --signal-text: var(--signal);          /* magenta on near-black measures 5.17:1 */

    --led-warn-edge: color-mix(in srgb, var(--led-warn) 34%, transparent);
    --led-on-edge: color-mix(in srgb, var(--led-on) 34%, transparent);
    --fault-edge: color-mix(in srgb, var(--fault) 44%, transparent);
    --signal-edge-soft: color-mix(in srgb, var(--signal) 46%, transparent);
    --knob-ring: rgba(255, 45, 149, .45);
}

/* ---------- lollypop ----------

   The opposite instinct: nothing is milled, nothing is lit. Soft cream ground,
   fully rounded corners, shadows that sit under things like sugar paper rather
   than cutting into them. Raspberry is the live colour and it is dark enough to
   carry white ink, because a candy theme that cannot be read is a toy.

   Ink stays near-black on cream: the sweetness is in the shapes and the accents,
   not in washing the text out to pastel. */
:root[data-theme="lollypop"] {
    --chassis: #ffe8f2;
    --plate: #fffafc;
    --plate-hi: #ffffff;
    --recess: #fff3f8;

    --engrave: #ffc4dd;
    --engrave-hi: #f79ac0;

    --ink: #3d1f30;
    --ink-2: #6b4457;
    --ink-3: #6d4054;                         /* holds 4.5:1 on the pink chassis */

    --signal: #d81b60;                        /* raspberry, dark enough for white ink */
    --signal-dim: rgba(216, 27, 96, .12);
    --led-on: #00897b;                        /* mint, darkened to read on cream */
    --led-warn: #b26a00;
    --fault: #c62828;

    --on-signal: #ffffff;
    --signal-edge: #ad1457;
    --grain: rgba(216, 27, 96, .02);
    --machined: rgba(216, 27, 96, .10);
    --machined-soft: rgba(216, 27, 96, .08);
    --drop: rgba(216, 27, 96, .22);
    --scroll-hover: #f79ac0;
    --fault-ink: #8e1c1c;
    --on-fault: #ffffff;
    --thumb-filter: saturate(1.1);
    --thumb-read: grayscale(.5) opacity(.8);
    --tile-l1: 88%;
    --tile-l2: 76%;
    --tile-ink-l: 34%;
    --tag-mix: black;
    --tag-ink-mix: 58%;
    --tag-ink-base: var(--ink);

    /* The restyle: everything rounds, and shadows sit under rather than inside. */
    --radius: 12px;
    --radius-key: 999px;                      /* keys become sweets */
    --glow: 0 2px 8px -2px rgba(216, 27, 96, .18);
    --glow-key: 0 3px 10px -2px rgba(216, 27, 96, .35);
    --label-font: var(--sans);
    --title-font: var(--sans);
    --title-weight: 600;                   /* rounder shapes need more weight to hold */
    --title-tracking: -.005em;
    --label-tracking: .12em;
    --border-width: 1.5px;

    /* Not a list of rows: a tray of separated sweets. No dividing rules at all —
       the gap and the shadow do that work. */
    --card-bg: var(--plate);
    --card-rule: none;
    --card-radius: 16px;
    --card-pad: .7rem .8rem;
    --card-gap: .55rem;
    --card-shadow: 0 2px 10px -4px rgba(216, 27, 96, .28);
    --card-shadow-hover: 0 6px 18px -6px rgba(216, 27, 96, .42);
    --thumb-radius: 10px;

    /* Raspberry small text measured 3.55:1 on the cream plate. */
    --signal-text: #a3134a;

    --led-warn-edge: color-mix(in srgb, var(--led-warn) 34%, transparent);
    --led-on-edge: color-mix(in srgb, var(--led-on) 32%, transparent);
    --fault-edge: color-mix(in srgb, var(--fault) 40%, transparent);
    --signal-edge-soft: color-mix(in srgb, var(--signal) 42%, transparent);
    --knob-ring: rgba(216, 27, 96, .3);
}

/* ---------- cartoon ----------

   Comic-book cel: flat blocks of colour, thick black outlines and a hard offset
   shadow with no blur, the way ink traps under a printing plate. Nothing here
   glows or gradients — a cel is opaque paint inside a drawn line.

   The black outline does the work the engrave hairline does elsewhere, so
   --engrave IS black and --border-width triples. */
:root[data-theme="cartoon"] {
    --chassis: #ffd93d;                       /* the page is the poster */
    --plate: #fffdf5;
    --plate-hi: #ffffff;
    --recess: #fff8e1;

    --engrave: #1a1a1a;                       /* the ink line */
    --engrave-hi: #000000;

    --ink: #1a1a1a;
    --ink-2: #3d3d3d;
    --ink-3: #4a4a4a;

    /* White on #e63946 measured 4.17:1 — a comic red bright enough to look right
       is not dark enough to carry white ink at 0.76rem. Deepened until it clears. */
    --signal: #cc1f2d;
    --signal-dim: rgba(204, 31, 45, .14);
    --led-on: #1b7f4b;
    --led-warn: #b8860b;
    --fault: #c1121f;

    --on-signal: #ffffff;
    --signal-edge: #1a1a1a;                   /* even the key is outlined in ink */
    --grain: rgba(0, 0, 0, .015);
    --machined: rgba(0, 0, 0, .12);
    --machined-soft: rgba(0, 0, 0, .1);
    --drop: rgba(0, 0, 0, .85);
    --scroll-hover: #1a1a1a;
    --fault-ink: #8b0000;
    --on-fault: #ffffff;
    --thumb-filter: saturate(1.3) contrast(1.1);
    --thumb-read: grayscale(.6) opacity(.8);
    --tile-l1: 86%;
    --tile-l2: 70%;
    --tile-ink-l: 26%;
    --tag-mix: black;
    --tag-ink-mix: 52%;
    --tag-ink-base: var(--ink);
    /* The active switch sits on a --signal-dim wash, not the bare plate, so the
       text needs more depth than the key does: #b3121e measured 4.14:1 there. */
    --signal-text: #8f0e17;

    --radius: 10px;
    --radius-key: 999px;
    --glow: 3px 3px 0 var(--engrave);         /* hard offset: ink, not light */
    --glow-key: 2px 2px 0 var(--engrave);
    --label-font: var(--sans);
    /* Bangers is a comic-lettering face; it only has a 400, and it is already
       heavy, so the weight comes down rather than up. */
    --title-font: "Bangers", "Impact", var(--sans);
    --title-weight: 400;
    --title-tracking: .01em;
    --title-size: 1.12rem;
    --label-tracking: .1em;
    --border-width: 2.5px;

    --card-bg: var(--plate);
    --card-rule: none;
    --card-radius: 14px;
    --card-pad: .7rem .8rem;
    --card-gap: .6rem;
    --card-shadow: 4px 4px 0 var(--engrave);
    --card-shadow-hover: 6px 6px 0 var(--engrave);
    --thumb-radius: 8px;

    --led-warn-edge: var(--engrave);
    --led-on-edge: var(--engrave);
    --fault-edge: var(--engrave);
    --signal-edge-soft: var(--engrave);
    --knob-ring: rgba(0, 0, 0, .3);
}

/* ---------- doodle ----------

   Ballpoint on graph paper. Everything looks drawn rather than printed: uneven
   corners, a wobbling underline, ink-blue on off-white. The whole point is that
   nothing lines up perfectly, which is the opposite of every other theme here.

   The irregular corners come from asymmetric border-radius; the paper grid comes
   from a repeating gradient on the chassis. */
:root[data-theme="doodle"] {
    --chassis: #f4f1e8;                       /* pad paper */
    --plate: #fffef9;
    --plate-hi: #ffffff;
    --recess: #faf8f0;

    --engrave: #9aa3b8;                       /* pencil rule */
    --engrave-hi: #5b6478;

    --ink: #232b3d;                           /* ballpoint blue-black */
    --ink-2: #414c66;
    --ink-3: #4d5772;

    --signal: #2f5fd0;                        /* biro blue */
    --signal-dim: rgba(47, 95, 208, .12);
    --led-on: #2e7d4f;
    --led-warn: #9a6b00;
    --fault: #c0392b;

    --on-signal: #ffffff;
    --signal-edge: #22499f;
    --grain: rgba(35, 43, 61, .02);
    --machined: rgba(35, 43, 61, .1);
    --machined-soft: rgba(35, 43, 61, .08);
    --drop: rgba(35, 43, 61, .3);
    --scroll-hover: #5b6478;
    --fault-ink: #8e2a1f;
    --on-fault: #ffffff;
    --thumb-filter: saturate(.9) contrast(.96);
    --thumb-read: grayscale(.7) opacity(.75);
    --tile-l1: 88%;
    --tile-l2: 76%;
    --tile-ink-l: 30%;
    --tag-mix: black;
    --tag-ink-mix: 55%;
    --tag-ink-base: var(--ink);
    --signal-text: #22499f;

    /* Nothing is drawn twice the same: every corner radius differs. */
    --radius: 14px 6px 12px 8px / 8px 14px 6px 12px;
    --radius-key: 12px 20px 10px 18px / 18px 10px 20px 12px;
    --glow: none;
    --glow-key: none;
    --label-font: var(--sans);
    --title-font: "PatrickHand", "Segoe Print", cursive, var(--sans);
    --title-weight: 400;
    --title-tracking: .005em;
    --title-size: 1.08rem;
    --label-tracking: .1em;
    --border-width: 2px;

    --card-bg: var(--plate);
    --card-rule: none;
    --card-radius: 18px 8px 16px 10px / 10px 18px 8px 16px;
    --card-pad: .7rem .8rem;
    --card-gap: .55rem;
    --card-shadow: 2px 2px 0 rgba(35, 43, 61, .12);
    --card-shadow-hover: 3px 3px 0 rgba(35, 43, 61, .2);
    --thumb-radius: 10px 4px 8px 6px / 6px 10px 4px 8px;

    --led-warn-edge: color-mix(in srgb, var(--led-warn) 40%, transparent);
    --led-on-edge: color-mix(in srgb, var(--led-on) 40%, transparent);
    --fault-edge: color-mix(in srgb, var(--fault) 45%, transparent);
    --signal-edge-soft: color-mix(in srgb, var(--signal) 45%, transparent);
    --knob-ring: rgba(35, 43, 61, .25);
}

/* ---------- matrix ----------

   Screen, not surface. Black ground, one green, and everything drawn in mono
   because on this faceplate every string is machine output. The panel does not
   pretend to be metal here: it is a terminal someone is reading over your shoulder. */
:root[data-theme="matrix"] {
    --chassis: #000600;
    --plate: #041007;
    --plate-hi: #06180b;
    --recess: #010a03;
    --engrave: #0d3f1c;
    --engrave-hi: #1a7a36;
    --ink: #b6ffc4;
    --ink-2: #5fd97e;
    --ink-3: #4bbf68;
    --signal: #00ff41;
    --signal-dim: rgba(0, 255, 65, .13);
    --led-on: #00e0a0;
    --led-warn: #b8ff5c;
    --fault: #ff4d4d;
    --on-signal: #001206;
    --signal-edge: #00b32e;
    --grain: rgba(0, 255, 65, .02);
    --machined: rgba(0, 0, 0, .7);
    --machined-soft: rgba(0, 0, 0, .55);
    --drop: rgba(0, 255, 65, .22);
    --scroll-hover: #1a7a36;
    --fault-ink: #ff9d9d;
    --on-fault: #180000;
    --thumb-filter: saturate(.5) sepia(.35) hue-rotate(65deg) contrast(1.08);
    --thumb-read: grayscale(.8) opacity(.6);
    --tile-l1: 24%;
    --tile-l2: 10%;
    --tile-ink-l: 70%;
    --tag-mix: white;
    --tag-ink-mix: 70%;
    --tag-ink-base: var(--ink-2);
    --signal-text: var(--signal);
    --radius: 0;
    --radius-key: 0;
    --glow: 0 0 12px -6px rgba(0, 255, 65, .5);
    --glow-key: 0 0 14px -3px rgba(0, 255, 65, .6);
    --label-font: var(--mono);
    --title-font: var(--sans);
    --title-weight: 500;
    --label-tracking: .22em;
    --border-width: 1px;
    --card-bg: rgba(4, 16, 7, .72);
    --card-rule: 1px solid #0d3f1c;
    --card-radius: 0;
    --card-pad: .55rem .5rem;
    --card-gap: .25rem;
    --card-shadow: none;
    --card-shadow-hover: inset 0 0 0 1px rgba(0, 255, 65, .35);
    --thumb-radius: 0;
    --led-warn-edge: color-mix(in srgb, var(--led-warn) 34%, transparent);
    --led-on-edge: color-mix(in srgb, var(--led-on) 34%, transparent);
    --fault-edge: color-mix(in srgb, var(--fault) 42%, transparent);
    --signal-edge-soft: color-mix(in srgb, var(--signal) 45%, transparent);
    --knob-ring: rgba(0, 255, 65, .4);
}

/* ---------- terminal ----------

   The amber CRT this design already half-believes it is. Phosphor on a warm-black
   tube, everything mono. Where matrix is a screen being read, this is a machine
   being operated. */
:root[data-theme="terminal"] {
    --chassis: #0d0800;
    --plate: #170f02;
    --plate-hi: #1f1503;
    --recess: #090500;
    --engrave: #4a3208;
    --engrave-hi: #7d5610;
    --ink: #ffd68a;
    --ink-2: #d9a94e;
    --ink-3: #c2953f;
    --signal: #ffb000;
    --signal-dim: rgba(255, 176, 0, .14);
    --led-on: #7fdd4c;
    --led-warn: #ffd166;
    --fault: #ff6b4a;
    --on-signal: #1a0f00;
    --signal-edge: #cc8d00;
    --grain: rgba(255, 176, 0, .022);
    --machined: rgba(0, 0, 0, .65);
    --machined-soft: rgba(0, 0, 0, .5);
    --drop: rgba(255, 176, 0, .25);
    --scroll-hover: #7d5610;
    --fault-ink: #ffb09c;
    --on-fault: #1a0500;
    --thumb-filter: sepia(.55) saturate(.85) hue-rotate(-12deg);
    --thumb-read: sepia(.8) opacity(.6);
    --tile-l1: 26%;
    --tile-l2: 11%;
    --tile-ink-l: 74%;
    --tag-mix: white;
    --tag-ink-mix: 72%;
    --tag-ink-base: var(--ink-2);
    --signal-text: var(--signal);
    --radius: 0;
    --radius-key: 0;
    --glow: 0 0 10px -6px rgba(255, 176, 0, .45);
    --glow-key: 0 0 12px -3px rgba(255, 176, 0, .55);
    --label-font: var(--mono);
    /* Mono titles were the point of a terminal, and they cost a title: at -.02em
       they clipped 2 of 25 at the two-line clamp, and tightening to -.045em and
       0.94rem still left 1. Squeezing further would have been shrinking the type
       to hide a bad fit. Same call as cyberpunk — the labels and readouts carry
       the machine character, the titles have to be read. */
    --title-font: var(--sans);
    --title-weight: 500;
    --title-tracking: 0;
    --label-tracking: .2em;
    --border-width: 1px;
    --card-bg: transparent;
    --card-rule: 1px solid #4a3208;
    --card-radius: 0;
    --card-pad: .5rem .4rem;
    --card-gap: 0;
    --card-shadow: none;
    --card-shadow-hover: none;
    --thumb-radius: 0;
    --led-warn-edge: color-mix(in srgb, var(--led-warn) 34%, transparent);
    --led-on-edge: color-mix(in srgb, var(--led-on) 34%, transparent);
    --fault-edge: color-mix(in srgb, var(--fault) 42%, transparent);
    --signal-edge-soft: color-mix(in srgb, var(--signal) 45%, transparent);
    --knob-ring: rgba(255, 176, 0, .35);
}

/* ---------- blueprint ----------

   Drafting linen: white ink on process blue, a drawn grid underneath, rules
   instead of fills. Nothing is solid — a blueprint describes an object, it is
   not the object. */
:root[data-theme="blueprint"] {
    --chassis: #0d3b66;
    --plate: #104b80;
    --plate-hi: #145a99;
    --recess: #0a3157;
    --engrave: #3d7cb5;
    --engrave-hi: #7fb2dd;
    --ink: #f2f8ff;
    --ink-2: #c2dcf2;
    --ink-3: #a8cbe8;
    --signal: #ffd166;
    --signal-dim: rgba(255, 209, 102, .16);
    --led-on: #7fe3b0;
    --led-warn: #ffd166;
    --fault: #ff9a8a;
    --on-signal: #123048;
    --signal-edge: #d9a93f;
    --grain: rgba(255, 255, 255, .015);
    --machined: rgba(0, 20, 40, .35);
    --machined-soft: rgba(0, 20, 40, .28);
    --drop: rgba(0, 20, 40, .5);
    --scroll-hover: #7fb2dd;
    --fault-ink: #ffc9bf;
    --on-fault: #2a0a05;
    --thumb-filter: saturate(.75) brightness(1.02);
    --thumb-read: grayscale(.6) opacity(.7);
    --tile-l1: 34%;
    --tile-l2: 20%;
    --tile-ink-l: 82%;
    --tag-mix: white;
    --tag-ink-mix: 62%;
    --tag-ink-base: var(--ink);
    --signal-text: var(--signal);
    --radius: 0;
    --radius-key: 0;
    --glow: none;
    --glow-key: none;
    --label-font: var(--mono);
    --title-font: var(--sans);
    --title-weight: 500;
    --label-tracking: .18em;
    --border-width: 1px;
    --card-bg: transparent;
    --card-rule: 1px solid #3d7cb5;
    --card-radius: 0;
    --card-pad: .55rem .5rem;
    --card-gap: 0;
    --card-shadow: none;
    --card-shadow-hover: inset 0 0 0 1px var(--engrave-hi);
    --thumb-radius: 0;
    --led-warn-edge: color-mix(in srgb, var(--led-warn) 40%, transparent);
    --led-on-edge: color-mix(in srgb, var(--led-on) 40%, transparent);
    --fault-edge: color-mix(in srgb, var(--fault) 45%, transparent);
    --signal-edge-soft: color-mix(in srgb, var(--signal) 45%, transparent);
    --knob-ring: rgba(255, 255, 255, .3);
}

/* ---------- newsprint ----------

   Broadsheet: warm grey stock, black ink, a serif for the headlines and rules
   that read as column dividers. The one place a serif is allowed, because a
   newspaper without one is not a newspaper. */
:root[data-theme="newsprint"] {
    --chassis: #d9d5cc;
    --plate: #f2efe8;
    --plate-hi: #faf8f3;
    --recess: #e8e4db;
    --engrave: #b0aaa0;
    --engrave-hi: #6b665e;
    --ink: #14120f;
    --ink-2: #3d3a35;
    --ink-3: #4a463f;
    --signal: #8b0000;
    --signal-dim: rgba(139, 0, 0, .1);
    --led-on: #1f6b3a;
    --led-warn: #8a6400;
    --fault: #a01818;
    --on-signal: #ffffff;
    --signal-edge: #5c0000;
    --grain: rgba(0, 0, 0, .018);
    --machined: rgba(0, 0, 0, .1);
    --machined-soft: rgba(0, 0, 0, .08);
    --drop: rgba(0, 0, 0, .25);
    --scroll-hover: #6b665e;
    --fault-ink: #7a1212;
    --on-fault: #ffffff;
    --thumb-filter: grayscale(1) contrast(1.15);
    --thumb-read: grayscale(1) opacity(.6);
    --tile-l1: 82%;
    --tile-l2: 68%;
    --tile-ink-l: 26%;
    --tag-mix: black;
    --tag-ink-mix: 50%;
    --tag-ink-base: var(--ink);
    --signal-text: #7a0000;
    --radius: 0;
    --radius-key: 0;
    --glow: none;
    --glow-key: none;
    --label-font: var(--sans);
    /* Bitter is a slab serif cut for text; Georgia was the stand-in. */
    --title-font: "Bitter", Georgia, "Times New Roman", serif;
    --title-weight: 700;
    --title-tracking: -.03em;
    --title-size: .93rem;
    --label-tracking: .14em;
    --border-width: 1px;
    --card-bg: transparent;
    --card-rule: 1px solid #b0aaa0;
    --card-radius: 0;
    --card-pad: .55rem .5rem;
    --card-gap: 0;
    --card-shadow: none;
    --card-shadow-hover: none;
    --thumb-radius: 0;
    --led-warn-edge: color-mix(in srgb, var(--led-warn) 40%, transparent);
    --led-on-edge: color-mix(in srgb, var(--led-on) 40%, transparent);
    --fault-edge: color-mix(in srgb, var(--fault) 45%, transparent);
    --signal-edge-soft: color-mix(in srgb, var(--signal) 45%, transparent);
    --knob-ring: rgba(0, 0, 0, .2);
}

/* ---------- vaporwave ----------

   Sunset chrome: magenta-to-cyan on deep indigo, soft glow, rounded everything.
   Cyberpunk's warmer cousin — that one is wet neon at night, this is an
   airbrushed sunset on a videotape. */
:root[data-theme="vaporwave"] {
    --chassis: #17093a;
    --plate: #241157;
    --plate-hi: #2f1970;
    --recess: #120630;
    --engrave: #4b2a94;
    --engrave-hi: #7d52d1;
    --ink: #f6ecff;
    --ink-2: #c9aef0;
    --ink-3: #b198e0;
    --signal: #ff5fa8;
    --signal-dim: rgba(255, 95, 168, .16);
    --led-on: #5fe0d8;
    --led-warn: #ffd98a;
    --fault: #ff6b8a;
    --on-signal: #1a0320;
    --signal-edge: #d63d86;
    --grain: rgba(255, 95, 168, .02);
    --machined: rgba(0, 0, 0, .5);
    --machined-soft: rgba(0, 0, 0, .4);
    --drop: rgba(255, 95, 168, .3);
    --scroll-hover: #7d52d1;
    --fault-ink: #ffb3c4;
    --on-fault: #1a0308;
    --thumb-filter: saturate(1.2) hue-rotate(-6deg);
    --thumb-read: brightness(.6) saturate(.7);
    --tile-l1: 32%;
    --tile-l2: 14%;
    --tile-ink-l: 78%;
    --tag-mix: white;
    --tag-ink-mix: 72%;
    --tag-ink-base: var(--ink-2);
    --signal-text: var(--signal);
    --radius: 8px;
    --radius-key: 999px;
    --glow: 0 0 20px -8px rgba(255, 95, 168, .5);
    --glow-key: 0 0 18px -4px rgba(255, 95, 168, .65);
    --label-font: var(--sans);
    --title-font: var(--sans);
    --title-weight: 500;
    --label-tracking: .22em;
    --border-width: 1px;
    --card-bg: rgba(36, 17, 87, .6);
    --card-rule: none;
    --card-radius: 12px;
    --card-pad: .65rem .75rem;
    --card-gap: .45rem;
    --card-shadow: 0 4px 18px -8px rgba(255, 95, 168, .45);
    --card-shadow-hover: 0 6px 24px -8px rgba(95, 224, 216, .5);
    --thumb-radius: 8px;
    --led-warn-edge: color-mix(in srgb, var(--led-warn) 34%, transparent);
    --led-on-edge: color-mix(in srgb, var(--led-on) 34%, transparent);
    --fault-edge: color-mix(in srgb, var(--fault) 42%, transparent);
    --signal-edge-soft: color-mix(in srgb, var(--signal) 45%, transparent);
    --knob-ring: rgba(255, 95, 168, .4);
}

/* ---------- forest ----------

   The quiet one. Deep green ground, warm plate, moss and bark rather than paint.
   Deliberate: the others shout, and something has to be restful to read a long
   transcript on. */
:root[data-theme="forest"] {
    --chassis: #101c14;
    --plate: #1a2c1f;
    --plate-hi: #22392a;
    --recess: #0c150f;
    --engrave: #35513c;
    --engrave-hi: #52795c;
    --ink: #e8f0e4;
    --ink-2: #b3c9b0;
    --ink-3: #9db89b;
    --signal: #d98e3c;
    --signal-dim: rgba(217, 142, 60, .14);
    --led-on: #6fbf73;
    --led-warn: #d9c05c;
    --fault: #d9705c;
    --on-signal: #1a1006;
    --signal-edge: #b3722c;
    --grain: rgba(255, 255, 255, .012);
    --machined: rgba(0, 0, 0, .45);
    --machined-soft: rgba(0, 0, 0, .35);
    --drop: rgba(0, 0, 0, .55);
    --scroll-hover: #52795c;
    --fault-ink: #f0b3a6;
    --on-fault: #1a0805;
    --thumb-filter: saturate(.95);
    --thumb-read: grayscale(.5) opacity(.7);
    --tile-l1: 26%;
    --tile-l2: 12%;
    --tile-ink-l: 74%;
    --tag-mix: white;
    --tag-ink-mix: 74%;
    --tag-ink-base: var(--ink-2);
    --signal-text: var(--signal);
    --radius: 6px;
    --radius-key: 6px;
    --glow: none;
    --glow-key: none;
    --label-font: var(--sans);
    /* The reading faceplate gets the reading face. */
    --title-font: "Bitter", Georgia, serif;
    --title-weight: 700;
    --title-tracking: -.02em;
    --title-size: .92rem;
    --label-tracking: .15em;
    --border-width: 1px;
    --card-bg: var(--plate);
    --card-rule: none;
    --card-radius: 8px;
    --card-pad: .6rem .7rem;
    --card-gap: .4rem;
    --card-shadow: 0 1px 3px -1px rgba(0, 0, 0, .4);
    --card-shadow-hover: 0 3px 10px -3px rgba(0, 0, 0, .5);
    --thumb-radius: 5px;
    --led-warn-edge: color-mix(in srgb, var(--led-warn) 34%, transparent);
    --led-on-edge: color-mix(in srgb, var(--led-on) 34%, transparent);
    --fault-edge: color-mix(in srgb, var(--fault) 42%, transparent);
    --signal-edge-soft: color-mix(in srgb, var(--signal) 45%, transparent);
    --knob-ring: rgba(255, 255, 255, .25);
}


/* ---------- per-theme ambient motion ----------

   The Single Motion Rule stands: channel-up is still the only ENTRANCE animation,
   and nothing below adds a second one. These are ambient — they belong to the
   faceplate, not to the content arriving — and they are deliberately absent on
   Dark and Light, where the instrument panel stays still.

   All of it sits on ::before on <body>, behind everything (z-index: -1) and
   pointer-events: none, so no layout and no interaction is affected. The
   prefers-reduced-motion block at the end of this file already collapses every
   animation in the app to 0.01ms, and these inherit that. */

:root[data-theme="matrix"] body::after,
:root[data-theme="terminal"] body::after,
:root[data-theme="vaporwave"] body::after {
    content: "";
    position: fixed;
    inset: 0;
    /* ::after, not ::before — ::before is already the brushed-metal grain layer,
       and a theme rule on it fought that instead of adding to it.

       z-index 0, not -1: a negative z-index puts the pseudo-element behind its
       own parent's background, and body carries an opaque --chassis fill, so the
       layer painted and was then covered. That is why none of this was visible.
       0 sits above the body fill and below the content, which all establishes
       stacking of its own. */
    z-index: 0;
    pointer-events: none;
}

/* Matrix: glyph rain — columns of falling light, drawn with gradients rather
   than characters, so it costs composited layers instead of a canvas full of
   text nodes.

   Three bugs before this worked, each found by counting pixels rather than
   trusting a screenshot:
     1. `background-size: 100% 100%` squashed the repeating pattern into one
        tile, so nothing repeated or scrolled.
     2. The stripes were drawn at 90deg (vertical) and animated vertically —
        vertical stripes slid up and down look identical to standing still.
     3. The layer sat on `body::before` at `z-index: -1`. ::before was ALREADY
        the brushed-metal grain, and a negative z-index puts a pseudo-element
        behind its own parent's background, which body fills opaquely. It
        painted, then got covered. Measured: one distinct colour in the frame.

   And once visible, a fourth: a single full-width falling gradient lights every
   column at the same instant, which reads as a scanner bar sweeping down rather
   than as rain. Rain needs columns falling independently, so the streams are
   separate elements at different speeds and offsets. */
:root[data-theme="matrix"] body::after {
    background-image:
        repeating-linear-gradient(90deg, rgba(0,255,65,.05) 0 1px, transparent 1px 26px);
    background-size: 26px 100%;
}

/* The falling streams. A real element rather than more background layers on the
   same box: CSS cannot animate individual background layers of one shorthand
   independently, and three streams that share a keyframe are one bar again. */
.rain {
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    overflow: hidden;
    display: none;                      /* only the matrix faceplate turns it on */
}
:root[data-theme="matrix"] .rain { display: block; }

.rain span {
    position: absolute;
    top: 0;
    width: 26px;
    height: 45vh;
    background: linear-gradient(180deg,
        transparent 0%,
        rgba(0, 255, 65, .18) 38%,
        rgba(170, 255, 205, .5) 50%,
        rgba(0, 255, 65, .18) 62%,
        transparent 100%);
    animation: rain-fall linear infinite;
}
@keyframes rain-fall {
    from { transform: translateY(-45vh); }
    to   { transform: translateY(100vh); }
}

/* Terminal: the scanline drift of a CRT very slightly out of sync. This one was
   drawn correctly — 0deg stripes moving vertically — but 3px of travel at .22
   alpha is a shimmer you feel rather than see, so the band is taller and the
   travel longer. */
:root[data-theme="terminal"] body::after {
    background-image: repeating-linear-gradient(
        0deg,
        rgba(0, 0, 0, .30) 0 2px,
        transparent 2px 5px);
    background-size: 100% 5px;
    animation: crt-drift 1.2s linear infinite;
}
@keyframes crt-drift {
    from { background-position: 0 0; }
    to   { background-position: 0 5px; }
}

/* Vaporwave: a slow sunset that never quite finishes setting. */
:root[data-theme="vaporwave"] body::after {
    background-image: linear-gradient(
        160deg,
        rgba(255, 95, 168, .16) 0%,
        rgba(95, 224, 216, .13) 45%,
        rgba(255, 95, 168, .16) 100%);
    background-size: 100% 300%;
    animation: sunset-drift 14s ease-in-out infinite alternate;
}
@keyframes sunset-drift {
    from { background-position: 0 0%; }
    to   { background-position: 0 100%; }
}


/* ---------- ambient motion for the remaining faceplates ----------

   Every theme except Dark and Light now has one. Dark and Light stay still on
   purpose: they are the instrument panel, and an instrument panel that shimmers
   is a broken instrument. All of these are ambient, none is an entrance, so the
   Single Motion Rule still holds — and all inherit the reduced-motion stop. */

:root[data-theme="cyberpunk"] body::after,
:root[data-theme="lollypop"] body::after,
:root[data-theme="cartoon"] body::after,
:root[data-theme="doodle"] body::after,
:root[data-theme="blueprint"] body::after,
:root[data-theme="newsprint"] body::after,
:root[data-theme="forest"] body::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
}

/* Cyberpunk: the sign's power is not quite clean — a slow magenta pulse from
   below, like a transformer humming. */
:root[data-theme="cyberpunk"] body::after {
    background-image: linear-gradient(0deg,
        rgba(255, 45, 149, .14) 0%,
        rgba(255, 45, 149, .04) 30%,
        transparent 60%);
    animation: neon-hum 6s ease-in-out infinite alternate;
}
@keyframes neon-hum {
    from { opacity: .45; }
    to   { opacity: 1; }
}

/* Lollypop: sweets drifting past, slow and soft. */
:root[data-theme="lollypop"] body::after {
    background-image:
        radial-gradient(circle at 12% 20%, rgba(216, 27, 96, .10) 0 38px, transparent 39px),
        radial-gradient(circle at 78% 62%, rgba(0, 137, 123, .09) 0 30px, transparent 31px),
        radial-gradient(circle at 45% 88%, rgba(216, 27, 96, .08) 0 44px, transparent 45px);
    background-size: 100% 100%;
    animation: candy-drift 22s ease-in-out infinite alternate;
}
@keyframes candy-drift {
    from { transform: translate3d(0, 0, 0) scale(1); }
    to   { transform: translate3d(2.5%, -3%, 0) scale(1.08); }
}

/* Cartoon: the whole cel jitters very slightly, the way hand-inked frames never
   register perfectly. Two pixels, and slow enough not to be annoying. */
:root[data-theme="cartoon"] body::after {
    background-image: radial-gradient(circle at 50% 0%,
        rgba(0, 0, 0, .05) 0%, transparent 60%);
    animation: cel-jitter .9s steps(2, end) infinite;
}
@keyframes cel-jitter {
    0%   { transform: translate(0, 0); }
    50%  { transform: translate(1px, -1px); }
    100% { transform: translate(0, 0); }
}

/* Doodle: the pencil grid breathes, as if the paper is being redrawn. */
:root[data-theme="doodle"] body::after {
    background-image:
        repeating-linear-gradient(0deg, transparent 0 23px, rgba(90, 110, 160, .07) 23px 24px);
    animation: paper-breathe 8s ease-in-out infinite alternate;
}
@keyframes paper-breathe {
    from { opacity: .3; transform: translateY(0); }
    to   { opacity: .9; transform: translateY(1px); }
}

/* Blueprint: the drafting sheet is being unrolled — a soft highlight travelling
   across it, the way a light moves over linen. */
:root[data-theme="blueprint"] body::after {
    background-image: linear-gradient(115deg,
        transparent 0%,
        rgba(255, 255, 255, .05) 45%,
        rgba(255, 255, 255, .09) 50%,
        rgba(255, 255, 255, .05) 55%,
        transparent 100%);
    background-size: 260% 100%;
    animation: sheet-sweep 11s linear infinite;
}
@keyframes sheet-sweep {
    from { background-position: -160% 0; }
    to   { background-position: 160% 0; }
}

/* Newsprint: the press. A faint halftone that shifts, as ink does between
   impressions. */
:root[data-theme="newsprint"] body::after {
    background-image: radial-gradient(circle, rgba(0, 0, 0, .05) 0 1px, transparent 1px);
    background-size: 4px 4px;
    animation: press-shift 3s steps(3, end) infinite;
}
@keyframes press-shift {
    0%   { background-position: 0 0; }
    33%  { background-position: 1px 1px; }
    66%  { background-position: 2px 0; }
    100% { background-position: 0 0; }
}

/* Forest: light through leaves. The slowest thing here by a distance — this is
   the theme for reading, and it must never pull the eye. */
:root[data-theme="forest"] body::after {
    background-image:
        radial-gradient(ellipse at 25% 10%, rgba(217, 142, 60, .07) 0 22%, transparent 45%),
        radial-gradient(ellipse at 72% 30%, rgba(111, 191, 115, .05) 0 18%, transparent 40%);
    animation: canopy 26s ease-in-out infinite alternate;
}
@keyframes canopy {
    from { transform: translate3d(0, 0, 0); opacity: .6; }
    to   { transform: translate3d(-1.5%, 1.5%, 0); opacity: 1; }
}

/* Blueprint: no motion, but the drawn grid the theme is named for. */
:root[data-theme="blueprint"] body {
    background-image:
        repeating-linear-gradient(0deg, transparent 0 27px, rgba(255, 255, 255, .06) 27px 28px),
        repeating-linear-gradient(90deg, transparent 0 27px, rgba(255, 255, 255, .06) 27px 28px);
}
/* Graph paper, only on the doodle theme. Behind everything, and cheap: two
   repeating gradients rather than an image. */
:root[data-theme="doodle"] body {
    background-image:
        repeating-linear-gradient(0deg, transparent 0 23px, rgba(90, 110, 160, .13) 23px 24px),
        repeating-linear-gradient(90deg, transparent 0 23px, rgba(90, 110, 160, .13) 23px 24px);
}

* { box-sizing: border-box; }

html {
    scrollbar-color: var(--engrave-hi) transparent;
    scrollbar-width: thin;
}

html, body {
    margin: 0;
    padding: 0;
    background: var(--chassis);
    color: var(--ink);
    font-family: var(--sans);
    font-size: 16px;
    line-height: 1.45;
    -webkit-font-smoothing: antialiased;
}

/* Brushed-metal grain: a fine vertical striation, almost subliminal. */
body::before {
    content: "";
    position: fixed;
    inset: 0;
    background: repeating-linear-gradient(90deg,
        var(--grain) 0 1px, transparent 1px 3px);
    pointer-events: none;
    z-index: 0;
}

::selection { background: var(--signal-dim); color: var(--ink); }

::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: var(--recess); }
::-webkit-scrollbar-thumb { background: var(--engrave-hi); border: 3px solid var(--recess); }
::-webkit-scrollbar-thumb:hover { background: var(--scroll-hover); }

:focus-visible { outline: 2px solid var(--signal); outline-offset: 1px; }

a { color: var(--ink); text-decoration: none; }

.icon { flex-shrink: 0; display: block; }

/* ---------- chassis ---------- */

.shell {
    position: relative;
    z-index: 1;
    max-width: 1600px;
    margin: 0 auto;
    padding: 0 1.5rem 4rem;
}

/* --- top bezel --- */

.topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 1.1rem 0 .9rem;
    border-bottom: 1px solid var(--engrave);
    flex-wrap: wrap;
}

.brand { display: inline-flex; align-items: center; gap: .5rem; }

.brand-mark {
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    background: var(--signal);
    color: var(--on-signal);
}

.brand-name {
    font-weight: 600;
    font-size: 1.02rem;
    letter-spacing: .09em;
    text-transform: uppercase;
}

/* Rack units are measured in U — the model number on the faceplate. */
.brand-unit {
    font-family: var(--mono);
    font-size: .64rem;
    color: var(--ink-3);
    border: 1px solid var(--engrave);
    padding: .05rem .3rem;
    margin-left: .1rem;
}

.topnav { display: flex; align-items: center; gap: 1.35rem; }

.topnav a {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    font-size: .74rem;
    font-weight: 500;
    letter-spacing: .13em;
    text-transform: uppercase;
    color: var(--ink-3);
    padding-bottom: 2px;
    border-bottom: 1px solid transparent;
    transition: color .12s var(--ease), border-color .12s var(--ease);
}
.topnav a:hover { color: var(--ink); }
.topnav a.active { color: var(--signal-text); border-bottom-color: var(--signal); }

:root[data-theme="light"] .ch-thumb {
    /* Bare dark images punch holes in a light plate; a soft edge seats them. */
    box-shadow: inset 0 0 0 1px var(--knob-ring);
}

/* Panel lighting: a selector, not a rocker. Two faceplates could be a toggle; four
   cannot, and a rocker that steps through them hides three of the choices behind
   repeated presses. The native select is deliberate — it is one tap on a phone,
   it is keyboard- and screen-reader-correct for free, and the panel already uses
   one for status. */
.theme-pick {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    color: var(--ink-3);
    cursor: pointer;
    transition: color .12s var(--ease);
}
.theme-pick:hover { color: var(--ink); }

.theme-pick-icon { flex-shrink: 0; }

.theme-pick-select {
    background: var(--recess);
    border: var(--border-width) solid var(--engrave);
    border-radius: var(--radius);
    box-shadow: inset 0 1px 2px var(--machined);
    color: inherit;
    font-family: var(--label-font);
    font-size: .7rem;
    font-weight: 500;
    letter-spacing: .08em;
    text-transform: uppercase;
    padding: .2rem 1.4rem .2rem .45rem;
    cursor: pointer;

    /* The chevron is drawn rather than native so it inherits the theme's ink
       instead of the browser's grey. */
    appearance: none;
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: right .62rem center, right .38rem center;
    background-size: 4px 4px, 4px 4px;
    background-repeat: no-repeat;
}
.theme-pick-select:focus-visible { outline: 2px solid var(--signal); outline-offset: 1px; }

/* The dropped list is painted by the OS, which does not inherit CSS variables;
   naming both colours keeps a dark theme's menu from opening white. */
.theme-pick-select option { background: var(--plate); color: var(--ink); }

.footer {
    display: flex;
    align-items: center;
    gap: .8rem;
    margin-top: 3rem;
    padding-top: .9rem;
    border-top: 1px solid var(--engrave);
    color: var(--ink-3);
    font-family: var(--mono);
    font-size: .68rem;
    letter-spacing: .04em;
}

/* Panel fixings, the detail that makes it a plate rather than a div. */
.plate-screw {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: radial-gradient(circle at 35% 30%, var(--engrave-hi), var(--recess) 70%);
    box-shadow: inset 0 0 0 1px var(--machined);
    flex-shrink: 0;
}
.footer span:nth-child(2) { flex: 1; }

/* ---------- silkscreen section labels ---------- */

.bay-label, .panel-label {
    display: flex;
    align-items: center;
    gap: .6rem;
    margin-bottom: .5rem;
    font-family: var(--label-font);
    font-size: .7rem;
    font-weight: 600;
    letter-spacing: var(--label-tracking);
    text-transform: uppercase;
    color: var(--ink-3);
}

.bay-hint, .switch-count {
    font-family: var(--mono);
    font-size: .62rem;
    letter-spacing: .02em;
    text-transform: none;
    color: var(--ink-3);
    opacity: .75;
}

.meter {
    font-family: var(--mono);
    font-size: .7rem;
    letter-spacing: .04em;
    color: var(--signal-text);
}

/* The engraved line that runs to the edge of the plate. */
.rule { flex: 1; height: 1px; background: var(--engrave); }

/* ---------- input bay ---------- */

.bay {
    margin-top: 1.6rem;
    background: var(--plate);
    border: var(--border-width) solid var(--engrave);
    border-top-color: var(--engrave-hi);   /* light catches the top edge */
    border-radius: var(--radius);
    box-shadow: var(--glow);
    padding: .85rem 1rem 1rem;
}

.bay-row { display: flex; gap: .5rem; flex-wrap: wrap; }

/* Recessed slot: the input is milled into the plate, so it sits darker than it. */
.slot {
    background: var(--recess);
    border: var(--border-width) solid var(--engrave);
    border-radius: var(--radius);
    box-shadow: inset 0 1px 2px var(--machined);
    color: var(--ink);
    font-family: var(--mono);
    font-size: .82rem;
    padding: .5rem .65rem;
    width: 100%;
}
.slot::placeholder { color: var(--ink-3); opacity: .8; }
.slot:focus { outline: none; border-color: var(--signal); }

.url-slot { flex: 1 1 22rem; }
.tag-slot { flex: 0 1 11rem; position: relative; }

/* The one live control on the panel. */
.key {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    background: var(--signal);
    border: var(--border-width) solid var(--signal-edge);
    border-radius: var(--radius-key);
    box-shadow: var(--glow-key);
    color: var(--on-signal);
    font-family: var(--label-font);
    font-size: .76rem;
    font-weight: 600;
    letter-spacing: .12em;
    text-transform: uppercase;
    padding: .5rem 1.1rem;
    cursor: pointer;
    transition: filter .12s var(--ease), transform .06s var(--ease);
}
.key:hover:not(:disabled) { filter: brightness(1.08); }
.key:active:not(:disabled) { transform: translateY(1px); }
.key:disabled { background: var(--plate-hi); border-color: var(--engrave); color: var(--ink-3); cursor: default; }
.key.small { font-size: .68rem; padding: .32rem .8rem; }

.key-led {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--on-signal);
    opacity: .55;
}
.key-led.busy { animation: blink .7s steps(2, end) infinite; }
@keyframes blink { 50% { opacity: .1; } }

.link-btn {
    background: none;
    border: none;
    color: var(--ink-3);
    font-family: var(--sans);
    font-size: .76rem;
    letter-spacing: .06em;
    text-transform: uppercase;
    cursor: pointer;
    padding: .3rem 0;
}
.link-btn:hover { color: var(--ink); }

/* ---------- readouts ---------- */

.readout {
    margin: .7rem 0 0;
    padding: .4rem .6rem;
    border-left: 2px solid var(--engrave-hi);
    background: var(--recess);
    font-family: var(--mono);
    font-size: .74rem;
    color: var(--ink-2);
}
.readout.ok    { border-left-color: var(--led-on); }
.readout.warn  { border-left-color: var(--led-warn); }
.readout.fault { border-left-color: var(--fault); color: var(--fault-ink); }

/* ---------- library panel ---------- */

.panel { margin-top: 2.2rem; }

.search-slot { position: relative; display: flex; align-items: center; flex: 0 0 20rem; }
.search-slot .slot { padding-left: 1.9rem; }

.slot-icon { position: absolute; left: .55rem; color: var(--ink-3); pointer-events: none; }

.slot-clear {
    position: absolute;
    right: .4rem;
    display: grid;
    place-items: center;
    width: 18px; height: 18px;
    background: none; border: none;
    color: var(--ink-3);
    cursor: pointer;
}
.slot-clear:hover { color: var(--ink); }

/* ---------- switches (tag filters) ---------- */

.switches { display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .9rem; }

.switch {
    display: inline-flex;
    align-items: baseline;
    gap: .3rem;
    background: var(--plate);
    border: var(--border-width) solid var(--engrave);
    border-radius: var(--radius-key);
    color: var(--ink-2);
    font-family: var(--mono);
    font-size: .7rem;
    letter-spacing: .02em;
    padding: .18rem .55rem;
    cursor: pointer;
    transition: color .12s var(--ease), border-color .12s var(--ease), background .12s var(--ease);
}
.switch:hover { color: var(--ink); border-color: var(--engrave-hi); }
.switch.on { background: var(--signal-dim); border-color: var(--signal); color: var(--signal-text); }
.switch.on .switch-count { color: var(--signal-text); opacity: .8; }
.switch.tiny { font-size: .65rem; padding: .1rem .42rem; }

/* A tag carries its own colour: a lamp on the left, the label tinted to match.
   Colour identifies the tag; it never competes with the signal amber, which the
   panel reserves for its one live control. */
.switch.tagged {
    border-color: color-mix(in srgb, var(--tag) 34%, var(--engrave));
    color: color-mix(in srgb, var(--tag) var(--tag-ink-mix), var(--tag-ink-base));
}
.switch.tagged::before {
    content: "";
    width: 6px;
    height: 6px;
    background: var(--tag);
    flex-shrink: 0;
}
.switch.tagged:hover {
    border-color: var(--tag);
    color: color-mix(in srgb, var(--tag) 92%, var(--tag-mix));
}
.switch.tagged.on {
    background: color-mix(in srgb, var(--tag) 16%, transparent);
    border-color: var(--tag);
    color: color-mix(in srgb, var(--tag) 88%, var(--tag-mix));
}
.switch.tagged.on .switch-count { color: inherit; opacity: .75; }

/* ---------- channel rows ---------- */

/* Channel strips run in columns when the panel is wide enough for a whole strip.
   440px is the narrowest a strip stays scannable; below that the columns would win
   and the titles would lose. */
.channels {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(440px, 1fr));
    column-gap: 1.6rem;
    /* Strips in a row share a height, so the engraved rules line up straight across
       the panel instead of stepping. The readout is pushed to the bottom of each
       strip, so those lines align too. */
    align-items: stretch;
    border-top: 1px solid var(--engrave);
}

/* While a strip is expanded for editing, stop stretching its neighbours to match —
   a strip whose thumbnail and controls are flung apart reads as broken. */
.channels:has(.channel.editing) {
    align-items: start;
}

.channel {
    display: grid;
    /* 140px is the ceiling, found by measuring: it is where the median row height (183px)
       meets the tallest row, i.e. the picture exactly fills the text block beside it and
       the list stays even. Below that rows are ragged; above it (160px -> 196px) every row
       grows only to hold a bigger image. Titles keep 391px of the 603px card. */
    grid-template-columns: 140px minmax(0, 1fr);
    gap: .8rem;
    align-items: start;
    position: relative;                       /* the number corners itself against this */
    padding: var(--card-pad, .5rem .4rem);
    border-bottom: var(--card-rule, 1px solid var(--engrave));
    background: var(--card-bg, transparent);
    border-radius: var(--card-radius, 0);
    box-shadow: var(--card-shadow, none);
    margin-bottom: var(--card-gap, 0);
    transition: background .12s var(--ease), box-shadow .12s var(--ease),
                transform .12s var(--ease);
}
.channel:hover { background: var(--plate); box-shadow: var(--card-shadow-hover, none); }
.channel.editing { background: var(--plate); }
.channel.read .ch-title { color: var(--ink-3); font-weight: 400; }
.channel.read .ch-thumb img { filter: var(--thumb-read); }
.channel.read:hover .ch-title { color: var(--ink-2); }
.channel.read:hover .ch-thumb img { filter: var(--thumb-filter); }

/* Engraved channel number. */
/* The number doubles as the select control, so it is a real button: focusable,
   announced by its aria-pressed state, and hit-target sized. It still has to read
   as a silkscreened number in the gutter, so it carries no chrome until you are
   near it. */
/* Bottom-left, a pill under the picture. It used to hold a 2.1rem gutter column
   open across every card for a number you rarely look at and a control you rarely
   use; that width now belongs to the title. Left rather than right because the
   thumbnail column is already reserved at 140px and usually ends higher than the
   text does — so the pill sits in space the card was spending anyway, instead of
   floating over the description. */
.ch-no {
    /* Sits on the foot of the PICTURE at any thumbnail size — 140px here, 96px on
       a phone. It is positioned against .ch-shot rather than the card: the card runs
       ~200px tall against an 88px image, so anchoring to the card put the pill level
       with the description instead of on the picture. width: max-content because a
       grid-placed button stretches to the full 140px column otherwise. */
    position: absolute;
    left: .3rem;
    bottom: .3rem;
    width: max-content;
    z-index: 1;
    background: none;
    border: var(--border-width) solid transparent;
    border-radius: 999px;                     /* a pill in every theme, not just the round one */
    padding: .05rem .4rem;
    font-family: var(--mono);
    font-size: .64rem;
    color: var(--ink-3);
    text-align: right;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
    transition: color .12s var(--ease), border-color .12s var(--ease),
                background .12s var(--ease);
}
.ch-no:not([aria-pressed="true"]) {
    opacity: .75;
    background: var(--plate);                 /* it can overlap the picture; give it a ground */
    border-color: var(--engrave);
}
.channel:hover .ch-no:not([aria-pressed="true"]) {
    opacity: 1;
    border-color: var(--engrave);
    color: var(--ink-2);
}
.ch-no:hover:not([aria-pressed="true"]) { border-color: var(--signal); color: var(--signal-text); }
.ch-no:focus-visible { outline: 2px solid var(--signal); outline-offset: 1px; }

/* Selected: the number lights, and the card says so too. The :not() guards above
   matter — hovering a selected row used to repaint its number back to the idle
   colours, so the one row you were pointing at was the one that stopped looking
   selected. */
.ch-no[aria-pressed="true"] {
    background: var(--signal);
    border-color: var(--signal);
    color: var(--on-signal);
}

/* The accent bar is declared on .channel:hover too, so it has to be restated for
   the hover case or pointing at a selected row drops its marker. */
.channel:has(.ch-no[aria-pressed="true"]),
.channel:has(.ch-no[aria-pressed="true"]):hover {
    background: var(--signal-dim);
    box-shadow: inset 3px 0 0 var(--signal);
}

/* Holds the picture and the number pill together, so the pill can sit on the
   image's foot rather than the card's. */
.ch-shot {
    grid-column: 1;
    grid-row: 1;
    position: relative;
    /* align-self matters: a grid item stretches to the row by default, and the pill
       anchored to its foot would then sit level with the description rather than on
       the picture. Shrink to the image. */
    align-self: start;
    line-height: 0;                 /* no inline-descender gap under the <a> */
}

.ch-thumb {
    display: block;
    position: relative;
    aspect-ratio: 16 / 10;
    overflow: hidden;
    background: var(--recess);
    border: var(--border-width) solid var(--engrave);
    border-radius: var(--thumb-radius, 0);
}
.ch-thumb img {
    width: 100%; height: 100%;
    object-fit: cover;
    display: block;
    filter: var(--thumb-filter);
    transition: filter .18s var(--ease);
}
.channel:hover .ch-thumb img { filter: none; }

.ch-letter {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    font-family: var(--mono);
    font-size: 1.35rem;
    font-weight: 700;
    color: hsl(var(--tile-hue, 210) 45% var(--tile-ink-l));
    background: linear-gradient(160deg,
        hsl(var(--tile-hue, 210) 26% var(--tile-l1)), hsl(var(--tile-hue, 210) 22% var(--tile-l2)));
}

.ch-body { grid-column: 2; grid-row: 1; min-width: 0; display: flex; flex-direction: column; height: 100%; }

.ch-title {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
    font-family: var(--title-font);
    font-size: var(--title-size, 1rem);
    font-weight: var(--title-weight, 500);
    line-height: 1.28;
    letter-spacing: var(--title-tracking, 0);
    color: var(--ink);
}
.ch-title:hover { color: var(--signal-text); }

.ch-desc {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 1;
    overflow: hidden;
    margin: .12rem 0 0;
    color: var(--ink-2);
    font-size: .82rem;
    line-height: 1.35;
}

.ch-tags { display: flex; flex-wrap: wrap; gap: .25rem; margin-top: .4rem; }

.ch-note {
    margin: .4rem 0 0;
    padding-left: .55rem;
    border-left: 1px solid var(--engrave-hi);
    color: var(--ink-2);
    font-family: var(--mono);
    font-size: .74rem;
    white-space: pre-wrap;
}

/* Readout: same place in every strip, so it is read without hunting. Inside the
   body rather than its own column, because a column strip has no width to spare. */
.ch-read {
    grid-column: 2;
    display: flex;
    align-items: baseline;
    gap: .5rem;
    margin-top: auto;      /* sits on the strip's floor, level across the row */
    padding-top: .35rem;
    font-family: var(--mono);
    font-size: .68rem;
    color: var(--ink-3);
    overflow: hidden;
}
.ch-site { color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 13rem; }
.ch-age { font-variant-numeric: tabular-nums; }

.ch-ctl {
    grid-column: 2;
    display: flex;
    align-items: center;
    gap: .3rem;
    margin-top: .35rem;
}

/* Status select styled as a labelled indicator with an LED. */
.status {
    appearance: none;
    background: var(--recess);
    border: 1px solid var(--engrave);
    color: var(--ink-3);
    font-family: var(--mono);
    font-size: .66rem;
    letter-spacing: .04em;
    padding: .2rem 1.25rem .2rem 1.35rem;
    cursor: pointer;
    background-image:
        radial-gradient(circle, var(--led) 45%, transparent 46%),
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 24 24' fill='none' stroke='%237b8796' stroke-width='3.5' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat, no-repeat;
    background-position: left .5rem center, right .42rem center;
    background-size: 5px 5px, 8px 8px;
}
.status { --led: var(--engrave-hi); }   /* unread: a dark lamp still reads as a lamp */
.status:hover { border-color: var(--engrave-hi); }
.status.reading { --led: var(--led-warn); color: var(--led-warn); border-color: var(--led-warn-edge); }
.status.read    { --led: var(--led-on);   color: var(--led-on);   border-color: var(--led-on-edge); }

/* 26px against a 0.68rem readout read as a toolbar bolted to the card rather than
   part of it — six of them plus the status select take a whole line on a narrow
   column. Sized to the row's own type instead. */
.ch-key {
    display: grid;
    place-items: center;
    width: 21px; height: 21px;
    background: var(--recess);
    border: var(--border-width) solid var(--engrave);
    border-radius: var(--radius);
    color: var(--ink-3);
    cursor: pointer;
    opacity: 0;
    transition: opacity .14s var(--ease), color .12s var(--ease), border-color .12s var(--ease);
}
.channel:hover .ch-key,
.channel:focus-within .ch-key { opacity: 1; }
.ch-key:hover { color: var(--ink); border-color: var(--engrave-hi); }
.ch-key.fault:hover { color: var(--fault); border-color: var(--fault-edge); }

/* ---------- tag field with suggestions ---------- */

.tagfield { position: relative; width: 100%; }

/* The suggestion list is a drop-down panel on the faceplate: same recess, same
   hairline, no rounding. */
.tagfield-list {
    position: absolute;
    top: calc(100% + 2px);
    left: 0;
    right: 0;
    z-index: 20;
    list-style: none;
    margin: 0;
    padding: 0;
    background: var(--recess);
    border: 1px solid var(--engrave-hi);
    box-shadow: 0 6px 18px -6px var(--drop);
    max-height: 13rem;
    overflow-y: auto;
}

.tagfield-option {
    display: flex;
    align-items: center;
    gap: .45rem;
    justify-content: space-between;
    gap: .6rem;
    width: 100%;
    background: none;
    border: none;
    border-bottom: 1px solid var(--engrave);
    color: var(--ink-2);
    font-family: var(--mono);
    font-size: .76rem;
    text-align: left;
    padding: .32rem .55rem;
    cursor: pointer;
}
.tagfield-list li:last-child .tagfield-option { border-bottom: none; }
.tagfield-option:hover,
.tagfield-option.active { background: var(--signal-dim); color: var(--signal-text); }

.tagfield-lamp {
    width: 6px;
    height: 6px;
    background: var(--tag, var(--engrave-hi));
    flex-shrink: 0;
}
.tagfield-name { flex: 1; }

.tagfield-count {
    font-size: .68rem;
    color: var(--ink-3);
    font-variant-numeric: tabular-nums;
}
.tagfield-option.active .tagfield-lamp {
    width: 6px;
    height: 6px;
    background: var(--tag, var(--engrave-hi));
    flex-shrink: 0;
}
.tagfield-name { flex: 1; }

.tagfield-count { color: var(--signal-text); opacity: .75; }

/* ---------- tag colour picker ---------- */

.tagcolors { display: grid; gap: .3rem; }

.tagcolor-row {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-wrap: wrap;
}
.tagcolor-row .switch { pointer-events: none; min-width: 5.5rem; }

.swatches { display: flex; flex-wrap: wrap; gap: .2rem; }

/* Square chips, no rounding — the panel has no round parts except its lamps. */
.swatch {
    width: 16px;
    height: 16px;
    background: var(--tag, var(--engrave));
    border: 1px solid var(--machined-soft);
    cursor: pointer;
    padding: 0;
    transition: transform .1s var(--ease), box-shadow .1s var(--ease);
}
.swatch:hover { transform: translateY(-1px); }
.swatch.on { box-shadow: 0 0 0 1px var(--chassis), 0 0 0 2px var(--ink-2); }

.swatch.reset {
    display: grid;
    place-items: center;
    background: var(--recess);
    border-color: var(--engrave-hi);
    color: var(--ink-3);
}
.swatch.reset:hover { color: var(--ink); }

.ch-edit { margin-top: .5rem; display: grid; gap: .3rem; }

/* Silkscreen field labels, matching the panel's section labels. */
.ch-edit-label {
    font-size: .6rem;
    font-weight: 600;
    letter-spacing: .15em;
    text-transform: uppercase;
    color: var(--ink-3);
    margin-top: .2rem;
}
.ch-edit-label:first-child { margin-top: 0; }
.ch-edit textarea { resize: vertical; line-height: 1.45; }
.ch-edit-keys { display: flex; align-items: center; gap: .6rem; }

/* One authored moment: the panel powering up. Rows are visible by default and the
   animation only lifts them, so a failed animation is invisible rather than blank. */
@keyframes channel-up {
    from { opacity: .4; transform: translateY(2px); }
    to   { opacity: 1;  transform: none; }
}
.channel {
    animation: channel-up .34s var(--ease) backwards;
    animation-delay: calc(var(--i, 0) * 22ms);
}

/* ---------- blank state ---------- */

.blank {
    padding: 3rem 1rem;
    text-align: center;
    color: var(--ink-3);
    border-bottom: 1px solid var(--engrave);
}
.blank-title {
    margin: 0 0 .3rem;
    font-size: .76rem;
    font-weight: 600;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--ink-2);
}
.blank p { margin: 0; font-size: .86rem; }
.blank a { color: var(--signal-text); border-bottom: 1px solid var(--signal-edge-soft); }

/* ---------- panel keys, bulk bar, selection ---------- */

/* A labelled key on the faceplate: smaller than the amber SAVE, still a real control. */
.panel-key {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    background: var(--plate);
    border: 1px solid var(--engrave);
    color: var(--ink-2);
    font-family: var(--sans);
    font-size: .66rem;
    font-weight: 600;
    letter-spacing: .13em;
    text-transform: uppercase;
    padding: .2rem .6rem;
    cursor: pointer;
    white-space: nowrap;
    transition: color .12s var(--ease), border-color .12s var(--ease);
}
.panel-key:hover:not(:disabled) { color: var(--ink); border-color: var(--engrave-hi); }
.panel-key:disabled { opacity: .45; cursor: default; }
.panel-key.fault { color: var(--fault); border-color: color-mix(in srgb, var(--fault) 35%, var(--engrave)); }
.panel-key.fault:hover { border-color: var(--fault); }
a.panel-key:hover { text-decoration: none; }

.bulkbar {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-wrap: wrap;
    margin-bottom: .8rem;
    padding: .5rem .6rem;
    background: var(--plate);
    border: 1px solid var(--signal);
}

.bulk-count {
    font-family: var(--mono);
    font-size: .7rem;
    color: var(--signal-text);
    white-space: nowrap;
}
.bulk-field { width: 9rem; position: relative; }
.bulk-gap { flex: 1; }

/* The channel number doubles as the select control: the checkbox appears on hover
   or when anything is selected, so an idle panel keeps its engraved numbers. */

.ch-num { font-variant-numeric: tabular-nums; }

.ch-key.has-snap { color: var(--led-on); border-color: color-mix(in srgb, var(--led-on) 30%, var(--engrave)); }
.ch-key.has-snap:hover { color: var(--led-on); border-color: var(--led-on); }
a.ch-key:hover { text-decoration: none; }

/* ---------- dead link ---------- */

.ch-dead {
    display: flex;
    align-items: center;
    gap: .3rem;
    margin: .4rem 0 0;
    color: var(--fault);
    font-family: var(--mono);
    font-size: .7rem;
}
.ch-dead a { color: var(--led-on); border-bottom: 1px solid color-mix(in srgb, var(--led-on) 40%, transparent); }

/* ---------- ai proposal ---------- */

/* Proposals are visibly provisional: dashed edge, and nothing is written until
   Accept is pressed. */
.proposal {
    margin-top: .5rem;
    padding: .5rem .6rem;
    background: var(--recess);
    border: var(--border-width) dashed var(--engrave-hi);
    border-radius: var(--radius);
}

.proposal-label {
    display: block;
    font-size: .6rem;
    font-weight: 600;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--ink-3);
    margin-bottom: .35rem;
}

/* The chips only reveal they are switches once one is struck through, which is
   too late for the first use. Say it in the label instead. */
.proposal-hint {
    margin-left: .5rem;
    font-weight: 400;
    letter-spacing: .04em;
    text-transform: none;
    opacity: .75;
}

.proposal-tags { display: flex; flex-wrap: wrap; gap: .25rem; margin-bottom: .35rem; }

/* A suggested tag is a switch you can throw, so it has to look pressable —
   otherwise it reads as a decision already made. Dropped ones stay visible and
   struck through rather than disappearing: a suggestion you rejected is
   information, and taking it back must not mean asking the model again. */
.proposal-tags .switch.tagged,
.proposal-new .switch.tagged { cursor: pointer; }

.switch.tagged.off {
    border-color: var(--engrave);
    color: var(--ink-3);
    text-decoration: line-through;
    text-decoration-thickness: 1px;
}
.switch.tagged.off::before { background: var(--ink-3); opacity: .5; }
.switch.tagged.off:hover {
    border-color: color-mix(in srgb, var(--tag) 34%, var(--engrave));
    color: color-mix(in srgb, var(--tag) var(--tag-ink-mix), var(--tag-ink-base));
    text-decoration: none;
}

.proposal-new {
    display: flex;
    align-items: center;
    gap: .35rem;
    margin: 0 0 .35rem;
    font-family: var(--mono);
    font-size: .68rem;
    color: var(--ink-3);
}

/* Room for a tag the model did not think of, so a near-miss suggestion does not
   send you to the row editor to type one word. */
.proposal-add { margin-bottom: .5rem; max-width: 22rem; }

.proposal-summary {
    margin: 0 0 .5rem;
    color: var(--ink-2);
    font-size: .82rem;
    line-height: 1.45;
}

/* The cost readout is ~158px and sits after Accept/Discard; on a phone that pushed
   the row 54px past the viewport and gave the whole page a horizontal scroll.
   Measured on iPhone 13 and Pixel 5. Let it wrap instead. */
.proposal-keys { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

/* ---------- prose (bookmarklet) ---------- */

.prose { max-width: 40rem; padding-top: 1.6rem; }
.prose h1 {
    font-size: .95rem;
    font-weight: 600;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--ink-2);
    margin: 0 0 .8rem;
    padding-bottom: .5rem;
    border-bottom: 1px solid var(--engrave);
}
.prose h2 {
    font-size: .72rem;
    font-weight: 600;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--ink-3);
    margin: 2rem 0 .5rem;
}
.prose p { color: var(--ink-2); font-size: .9rem; }
.prose ul { padding-left: 1rem; color: var(--ink-2); font-size: .88rem; }
.prose li { margin: .3rem 0; }
.prose code {
    font-family: var(--mono);
    font-size: .8em;
    background: var(--recess);
    border: 1px solid var(--engrave);
    padding: .05em .3em;
}

.drag-row { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin: 1.2rem 0; }

.bookmarklet-link {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    background: var(--signal);
    border: 1px solid var(--signal-edge);
    color: var(--on-signal) !important;
    font-size: .76rem;
    font-weight: 600;
    letter-spacing: .12em;
    text-transform: uppercase;
    padding: .5rem 1.1rem;
    cursor: grab;
}
.bookmarklet-link:active { cursor: grabbing; }

.hint { color: var(--ink-3); font-family: var(--mono); font-size: .74rem; }

.code {
    width: 100%;
    background: var(--recess);
    border: 1px solid var(--engrave);
    box-shadow: inset 0 1px 2px var(--machined);
    color: var(--ink-2);
    padding: .6rem .7rem;
    resize: vertical;
    font-family: var(--mono);
    font-size: .72rem;
    line-height: 1.5;
}

/* ---------- save popup ---------- */

.popup { max-width: 28rem; padding-top: 1.4rem; }
.popup h1 {
    font-size: .8rem;
    font-weight: 600;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--ink-2);
    margin: 0 0 1rem;
}
.ok-title { color: var(--led-on); }

.field { display: block; margin-bottom: .7rem; }
.field > span {
    display: block;
    font-size: .66rem;
    font-weight: 600;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--ink-3);
    margin-bottom: .25rem;
}
.field em { font-style: normal; opacity: .7; }
.field input {
    width: 100%;
    background: var(--recess);
    border: 1px solid var(--engrave);
    box-shadow: inset 0 1px 2px var(--machined);
    color: var(--ink);
    font-family: var(--mono);
    font-size: .82rem;
    padding: .45rem .6rem;
}
.field input:focus { outline: none; border-color: var(--signal); }
.readonly { color: var(--ink-3); }

.key.wide { width: 100%; justify-content: center; margin-top: .5rem; }

.selection {
    margin: 0;
    padding: .45rem .65rem;
    background: var(--recess);
    border-left: 2px solid var(--engrave-hi);
    color: var(--ink-2);
    font-family: var(--mono);
    font-size: .76rem;
    max-height: 7rem;
    overflow: auto;
}

.saved-title { font-weight: 500; margin: .3rem 0; }
.saved-file { color: var(--ink-3); font-family: var(--mono); font-size: .74rem; margin: 0 0 .7rem; }
.close-hint { color: var(--ink-3); font-size: .8rem; margin-top: 1rem; }
.tags { display: flex; flex-wrap: wrap; gap: .25rem; }
.tag {
    background: var(--plate);
    border: 1px solid var(--engrave);
    color: var(--ink-2);
    font-family: var(--mono);
    font-size: .68rem;
    padding: .1rem .45rem;
}

/* ---------- responsive ---------- */

@media (max-width: 720px) {
    .shell { padding: 0 .9rem 3rem; }

    /* One strip per row below this width; a 440px minimum would overflow a phone. */
    .channels { grid-template-columns: 1fr; }

    .channel {
        /* Bigger here too, but not 140: a phone column is ~355px, and the title needs
           the rest of it. */
        grid-template-columns: 96px minmax(0, 1fr);
        gap: .7rem;
        padding: .75rem .1rem;
    }
    /* The number is panel dressing, but it is also the select control, and touch
       has no hover to reveal it — so on a phone it stays put and stays visible. */
    .ch-no { opacity: 1; }
    /* Touch has no hover, so the controls must stay visible. */
    .ch-key { opacity: 1; }

    .tag-slot, .search-slot { flex: 1 1 100%; }
    .key { flex: 1 1 100%; justify-content: center; }

    /* Measured: the page scrolled sideways on a phone — 509px of content in a 390px
       viewport. The nav is five items (Library, Bookmarklet, Export, spend, theme) in a
       flex row that could not fold, so it alone was 493px wide. */
    .topnav {
        flex-wrap: wrap;
        gap: .5rem .9rem;
        justify-content: flex-start;
    }

    /* The panel header holds the search box, and the breakpoint above already asks that
       box to take a full row — but the header could not wrap either, so the box was
       squeezed to 43px at x=385: five pixels of it on screen. */
    .bay-label, .panel-label { flex-wrap: wrap; }

    /* The engraved rule earns its space on a wide plate; on a phone it only pushes the
       search box off the edge. */
    .panel-label .rule { display: none; }

    /* Touch targets. A 14px icon in tight padding is fine for a mouse and not for a
       thumb; six sit in a row on a card, and a mis-tap here does not miss — it hits the
       next button, one of which is Delete.
       40px rather than the usual 44: six 44px keys plus gaps is 294px and would not fit
       beside the status select. 6*40 + gaps = ~270px inside a ~355px column, and the row
       wraps as insurance. */
    .ch-key, .panel-key, .slot-clear {
        min-width: 40px;
        min-height: 40px;
        justify-content: center;
    }
    .ch-ctl { flex-wrap: wrap; gap: .3rem; }
}

/* Touch has no hover, and viewport width is a poor proxy for it — a 1024px tablet has no
   hover, an 800px desktop window does. Keying off the input type is what actually means
   "these controls can never be revealed by pointing". */
@media (hover: none) {
    .ch-key { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important;
        transition-duration: .01ms !important;
        /* A .01ms duration on an infinite animation still loops — it just loops
           very fast, which is worse than the motion it replaced. The ambient
           faceplate effects are all infinite, so they need stopping outright. */
        animation-iteration-count: 1 !important;
    }

    /* The ambient layer keeps its look and loses its movement. */
    /* Every ambient faceplate layer, and the rain's streams. */
    body::after, .rain span {
        animation: none !important;
    }
}

/* ---------- blazor error ui ---------- */

#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0; left: 0; right: 0;
    z-index: 1000;
    background: var(--fault);
    color: var(--on-fault);
    font-family: var(--mono);
    font-size: .78rem;
    padding: .6rem 1.2rem;
}
#blazor-error-ui .dismiss { cursor: pointer; float: right; }

/* ---------- ai meter (header) ---------- */

/* A small readout on the faceplate, beside the theme switch. Reads as an instrument,
   not a nav link: monospace figures, engraved surround, no underline. */
.spend {
    display: inline-flex;
    align-items: baseline;
    gap: .35rem;
    padding: .16rem .5rem;
    border: 1px solid var(--engrave);
    background: var(--recess);
    font-family: var(--mono);
    line-height: 1;
    white-space: nowrap;
}
.spend:hover { border-color: var(--engrave-hi); text-decoration: none; }

/* Deliberately not amber: this design spends its one accent colour on the live
   control (SAVE), and a passive readout that shouts competes with it. */
.spend-value { font-size: .74rem; color: var(--ink-2); }
.spend:hover .spend-value { color: var(--ink); }

.spend-label {
    font-size: .58rem;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--ink-3);
}

/* ---------- ai usage page ---------- */

.gauges {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
    gap: 1px;
    margin-top: .8rem;
    background: var(--engrave);
    border: 1px solid var(--engrave);
}

/* Each figure sits in its own milled recess, so the row reads as four instruments
   rather than one run of text. */
.gauge {
    display: flex;
    flex-direction: column;
    gap: .3rem;
    padding: .7rem .8rem;
    background: var(--recess);
}

.gauge-value {
    font-family: var(--mono);
    font-size: 1.4rem;
    line-height: 1;
    color: var(--ink);
}
/* The headline figure is amber in both themes, but not the SAME amber: measured,
   --signal gives 9.81:1 on the dark plate and only 3.49:1 on white. The light theme
   gets a darker burnt amber so the most important number on the page is never the
   least legible one. */
.gauge:first-child .gauge-value { color: var(--signal-text); }

.gauge-label {
    font-size: .58rem;
    font-weight: 600;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--ink-3);
}

/* The subscription caveat. Amber-edged because it qualifies the headline number:
   without it the figure reads as money spent, which on a subscription it is not. */
.spend-note {
    margin: .7rem 0 0;
    padding: .45rem .7rem;
    border-left: 2px solid var(--engrave-hi);
    background: var(--recess);
    font-size: .76rem;
    line-height: 1.5;
    color: var(--ink-2);
}
.spend-note-equiv { border-left-color: var(--signal); }

.spend-backend {
    display: block;
    margin-top: .2rem;
    font-family: var(--mono);
    font-size: .68rem;
    color: var(--ink-3);
}

.empty {
    margin: 0;
    padding: 1.4rem .2rem;
    color: var(--ink-3);
    font-size: .84rem;
}

/* ---------- data grid ---------- */

.grid {
    width: 100%;
    margin-top: .8rem;
    border-collapse: collapse;
    font-size: .78rem;
}

.grid th {
    padding: .4rem .6rem;
    border-bottom: 1px solid var(--engrave-hi);
    font-size: .58rem;
    font-weight: 600;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--ink-3);
    text-align: left;
    white-space: nowrap;
}

.grid td {
    padding: .4rem .6rem;
    border-bottom: 1px solid var(--engrave);
    color: var(--ink-2);
    vertical-align: middle;
}

.grid tbody tr:hover td { background: var(--plate); }

/* Figures line up on the decimal point when they are right-aligned and monospaced;
   scanning a column for the expensive row depends on it. */
.grid .num {
    text-align: right;
    font-family: var(--mono);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.grid .dim { color: var(--ink-3); }
.grid .when,
.grid .model { font-family: var(--mono); font-size: .72rem; color: var(--ink-3); }

.grid code {
    font-family: var(--mono);
    font-size: .72rem;
    color: var(--ink);
}

/* Share bar: proportion at a glance, no percentage to read. */
.share {
    display: inline-block;
    width: 5rem;
    height: 6px;
    background: var(--recess);
    border: 1px solid var(--engrave);
    vertical-align: middle;
}

.share-fill {
    display: block;
    height: 100%;
    background: var(--signal);
}

/* What one proposal cost, beside its Accept/Discard keys. */
.proposal-cost {
    margin-left: auto;
    font-family: var(--mono);
    font-size: .66rem;
    color: var(--ink-3);
    white-space: nowrap;
}

/* ---------- option rows (video archive choice) ---------- */

.opt {
    display: flex;
    align-items: flex-start;
    gap: .5rem;
    margin-bottom: .45rem;
    cursor: pointer;
    font-size: .8rem;
    color: var(--ink-2);
    line-height: 1.4;
}
.opt input { margin-top: .15rem; accent-color: var(--signal); cursor: pointer; }

/* The consequence of the choice, not a restatement of the label. */
.opt em {
    display: block;
    font-style: normal;
    font-size: .72rem;
    color: var(--ink-3);
}

.opt.off { cursor: default; }
.opt.off input { cursor: default; }

.opt-num {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin: 0 0 .55rem 1.5rem;
    font-size: .74rem;
    color: var(--ink-3);
}
.slot.tiny { width: 4.5rem; padding: .2rem .4rem; font-size: .78rem; }

/* The film key stays lit while its panel is open. */
.ch-key.on { color: var(--signal-text); border-color: var(--signal); }

/* ---------- followed channels ---------- */

.feeds {
    margin-bottom: .8rem;
    padding: .6rem .7rem;
    background: var(--plate);
    border: 1px solid var(--engrave);
}

.feeds-add { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.feeds-add .slot { flex: 1 1 18rem; }

.feeds-empty {
    margin: .7rem 0 0;
    font-size: .78rem;
    line-height: 1.5;
    color: var(--ink-3);
}

.feed-list { list-style: none; margin: .7rem 0 0; padding: 0; }

.feed-row {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .35rem 0;
    border-top: 1px solid var(--engrave);
    font-size: .8rem;
}

.feed-name { color: var(--ink); }
.feed-when { margin-left: auto; font-family: var(--mono); font-size: .68rem; color: var(--ink-3); }

.panel-key.on { color: var(--signal-text); border-color: var(--signal); }
.panel-key.new-key { color: var(--signal-text); border-color: var(--signal); }

/* A lit marker on a video that arrived while you were not looking. It clears the
   moment the link is opened, so it means "not seen yet", not "recent". */
.newlamp {
    display: inline-block;
    margin-right: .4rem;
    padding: .05rem .3rem;
    background: var(--signal);
    color: var(--on-signal);
    font-family: var(--mono);
    font-size: .58rem;
    font-weight: 700;
    letter-spacing: .1em;
    text-transform: uppercase;
    vertical-align: middle;
}

/* The away bookmarklet is the fragile one — present, but not the first thing the eye
   lands on, since the home one is what should be installed. */
.bookmarklet-link.away {
    background: var(--plate);
    color: var(--ink-2);
    border-color: var(--engrave-hi);
}
.bookmarklet-link.away:hover { color: var(--ink); }

/* State switches sit above the tag switches: what you have dealt with, rather than what
   something is about. Slightly recessed so the tag row stays the louder one. */
.switches.states { margin-bottom: .35rem; }
.switches.states .switch { border-style: dashed; }
.switches.states .switch.on { border-style: solid; }

/* New is the one that should catch the eye — it is the incoming queue. */
.switch.state-new { color: var(--signal-text); border-color: color-mix(in srgb, var(--signal) 45%, var(--engrave)); }
.switch.state-new.on { background: var(--signal); color: var(--on-signal); border-color: var(--signal); }

/* ---------- ask ---------- */

.ask-hint {
    margin: .6rem 0 0;
    font-size: .78rem;
    line-height: 1.5;
    color: var(--ink-3);
}

/* A readout, per DESIGN.md: mono at 0.7rem for "every number and machine fact". */
.ask-cost {
    font-family: var(--mono);
    font-size: .7rem;
    color: var(--ink-3);
    white-space: nowrap;
}

.ask-answer {
    max-width: 40rem;
    margin-top: .8rem;
    font-size: .9rem;
    line-height: 1.7;
    color: var(--ink);
}
.ask-answer p { margin: 0 0 1rem; }

.ask-sources-label { margin-top: 1.4rem; }

.ask-sources { list-style: none; margin: 0; padding: 0; }

.ask-source {
    display: flex;
    align-items: baseline;
    gap: .6rem;
    padding: .4rem 0;
    border-top: 1px solid var(--engrave);
}

.ask-source-title { color: var(--ink); font-size: .85rem; }

.ask-source-site {
    margin-left: auto;
    font-family: var(--mono);
    font-size: .68rem;
    color: var(--ink-3);
}

/* ---------- discover ---------- */

.finds { list-style: none; margin: .6rem 0 0; padding: 0; }

.find {
    display: grid;
    grid-template-columns: 7rem minmax(0, 1fr) auto;
    gap: .8rem;
    align-items: center;
    padding: .5rem .2rem;
    border-bottom: 1px solid var(--engrave);
}

/* The number that earned the ranking, in the readout face — the whole point is that you
   can see WHY something is near the top. */
.find-score {
    font-family: var(--mono);
    font-size: .7rem;
    color: var(--signal-text);
    text-align: right;
    white-space: nowrap;
}

.find-title { color: var(--ink); font-size: .9rem; line-height: 1.35; }
.find-title:hover { color: var(--signal-text); }

.find-meta {
    display: flex;
    flex-wrap: wrap;
    gap: .15rem .7rem;
    margin: .15rem 0 0;
    font-family: var(--mono);
    font-size: .68rem;
    color: var(--ink-3);
}

/* Already saved: shown, not offered. Knowing you kept this one is part of judging the rest.
   Recede by muting the ink rather than with opacity — opacity fades the whole row including
   its label, which measured 2.95:1 on the light plate and failed AA. */
.find.known .find-title { color: var(--ink-2); }
.find.known .find-score { color: var(--ink-3); }

/* Not --led-on: that token is tuned for the plate and measures 2.95:1 on the chassis
   these rows sit on, which fails AA. This is a status label, not a lamp — the muted ink
   already proven on this background says "already yours" without shouting. */
.find-known {
    font-family: var(--mono);
    font-size: .68rem;
    color: var(--ink-3);
    white-space: nowrap;
}

@media (max-width: 720px) {
    .find { grid-template-columns: minmax(0, 1fr) auto; }
    .find-score { grid-column: 1 / -1; text-align: left; }
}

/* How old a finding is, next to the score that ranked it — the two facts you need to
   judge whether something is worth keeping. */
.find-age { color: var(--ink-2); }
