/*
 * An instrument panel, not a storefront.
 *
 * The job of this screen is scanning many candidates quickly, so density and
 * alignment matter more than decoration. Numbers use tabular figures so columns
 * compare vertically at a glance, and colour is reserved for evidence state:
 * anything coloured means something is true about the data, never decoration.
 *
 * Type runs small (13px base, 12px in the grid) because the reader is scanning
 * sixty rows, not reading one; the system UI faces stay legible at that size
 * where a webfont would not. Every animation is a state change the user caused.
 */

:root {
  --ground: #eef1f0;
  --surface: #ffffff;
  --surface-2: #f5f8f7;
  --surface-3: #e8eeec;
  --ink: #0f1817;
  --ink-2: #47554f;
  --ink-3: #78857f;
  --line: #d4dcd9;
  --line-strong: #b6c2be;
  --accent: #14566e;
  --accent-soft: #dbe8ed;
  --ok: #2a6b4a;
  --ok-soft: #dcede3;
  --warn: #8e5f16;
  --warn-soft: #f4e8d2;
  --crit: #9b3a32;
  --crit-soft: #f5dedb;
  --gem: #5f4b8b;
  --gem-soft: #e8e2f4;
  --shadow: 0 8px 28px rgba(15, 24, 23, .16);
  /*
   * The rest of the palette, added when the audit went to zero literals.
   *
   * A colour written anywhere but here is a colour that stays light when the
   * page goes dark, and the ones that hide longest are the ones nobody looks
   * at in the working theme: a backdrop, a drawer shadow, a focus ring.
   */
  --theme-color: #eef1f0;
  --link: #14566e;
  --link-visited: #5f4b8b;
  --select-bg: #cfe2ea;
  --select-ink: #0f1817;
  /*
   * The control tints: a mild colour code keyed to what a control DOES.
   *
   * Two jobs share the results bar and the rail. A control that chooses WHICH
   * products are in the result takes the accent hue, because accent already
   * means "you narrowed something" here: a ticked facet row, the filter count
   * badge and the clear button all carry it. A control that chooses the ORDER
   * takes the gem hue, the one palette hue no evidence verdict claims.
   * Presentation controls take neither, so the untinted one is the one with no
   * claim on the result.
   *
   * Both fills are weaker than the `-soft` pair they descend from, and that is
   * the whole constraint: colour in this UI means the data said something, so a
   * sort box that competes with a red risk verdict two rows down is a defect.
   * The text on them is --ink-2 at rest and --ink on hover, unchanged. Measured
   * on the rendered controls, the worst resting pair is 6.87:1 (the scope box
   * in dark) and the rest sit between 7.0 and 7.8; hover clears 13:1 in both
   * themes. Any future value has to hold that line, since the wash is what the
   * label sits on.
   */
  --scope-tint: #eff5f8;
  --scope-line: #b3cbd6;
  --scope-line-strong: #8fb2c1;
  --order-tint: #f4f1fa;
  --order-line: #c5bcdd;
  --order-line-strong: #a99cca;
  /*
   * The pad behind a product photo, and the one token that does NOT switch.
   * The CDN composites every image onto white, so a dark pad puts a bright
   * rectangle inside a dark card. The images are white; the page is not.
   */
  --image-pad: #ffffff;
  /*
   * The two grounds the System swatch has to show AT ONCE, which is why they
   * are the one pair no theme block redefines. Every other preview is painted
   * by the theme it selects; System has no palette of its own, so it shows the
   * question instead: this is what your device would give you.
   */
  --face-day: #eef1f0;
  --face-night: #0c1112;
  --backdrop: rgba(8, 14, 14, .48);
  --scrim: rgba(8, 14, 14, .4);
  --shadow-xs: 0 1px 2px rgba(0, 0, 0, .12);
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, .3);
  --shadow-md: 0 6px 20px rgba(0, 0, 0, .18);
  --shadow-lg: 0 24px 80px rgba(0, 0, 0, .4);
  --shadow-fab: 0 10px 30px rgba(0, 0, 0, .35);
  --shadow-drawer: 24px 0 60px rgba(0, 0, 0, .25);
  --mono: ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --rail: 236px;
  --topbar: 46px;
  /*
   * Declared, not implied. Without it the browser paints its own light canvas
   * behind a dark page during load and renders every native control light.
   */
  color-scheme: light;
}

/*
 * Dark, in two blocks, and the pair is the whole mechanism.
 *
 *  1. Under `prefers-color-scheme: dark`, guarded by
 *     `:root:not([data-theme="light"])` so a visitor who explicitly chose
 *     light still gets light on a dark device.
 *  2. Under `:root[data-theme="dark"]`, so an explicit dark choice wins on a
 *     light device.
 *
 * Neither block introduces a token name. Every name is defined in the bare
 * `:root` above, because a token whose only definition sits inside a media
 * query resolves to nothing for whoever does not match it.
 */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ground: #0c1112;
    --surface: #141b1c;
    --surface-2: #1b2425;
    --surface-3: #232e2f;
    --ink: #e7eeec;
    --ink-2: #a2afab;
    --ink-3: #6e7b77;
    --line: #283334;
    --line-strong: #3a4749;
    --accent: #63b8d6;
    --accent-soft: #16333d;
    --ok: #57ae81;
    --ok-soft: #16301f;
    --warn: #d3a24c;
    --warn-soft: #33260f;
    --crit: #e27a70;
    --crit-soft: #361a17;
    --gem: #b3a1dd;
    --gem-soft: #262038;
    --shadow: 0 8px 28px rgba(0, 0, 0, .5);
    --theme-color: #0c1112;
    --link: #7fc9e3;
    --link-visited: #c3b3e6;
    --select-bg: #1d4453;
    --select-ink: #e7eeec;
    /* Darker and flatter, not the light wash reused. A tint that reads as a
       whisper on white reads as a lit panel on near-black. */
    --scope-tint: #16262c;
    --scope-line: #33505c;
    --scope-line-strong: #48707f;
    --order-tint: #201d2b;
    --order-line: #453d5c;
    --order-line-strong: #5d5279;
    --backdrop: rgba(0, 0, 0, .62);
    --scrim: rgba(0, 0, 0, .55);
    --shadow-xs: 0 1px 2px rgba(0, 0, 0, .5);
    --shadow-sm: 0 1px 3px rgba(0, 0, 0, .6);
    --shadow-md: 0 6px 20px rgba(0, 0, 0, .55);
    --shadow-lg: 0 24px 80px rgba(0, 0, 0, .7);
    --shadow-fab: 0 10px 30px rgba(0, 0, 0, .6);
    --shadow-drawer: 24px 0 60px rgba(0, 0, 0, .6);
    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --ground: #0c1112;
  --surface: #141b1c;
  --surface-2: #1b2425;
  --surface-3: #232e2f;
  --ink: #e7eeec;
  --ink-2: #a2afab;
  --ink-3: #6e7b77;
  --line: #283334;
  --line-strong: #3a4749;
  --accent: #63b8d6;
  --accent-soft: #16333d;
  --ok: #57ae81;
  --ok-soft: #16301f;
  --warn: #d3a24c;
  --warn-soft: #33260f;
  --crit: #e27a70;
  --crit-soft: #361a17;
  --gem: #b3a1dd;
  --gem-soft: #262038;
  --shadow: 0 8px 28px rgba(0, 0, 0, .5);
  --theme-color: #0c1112;
  --link: #7fc9e3;
  --link-visited: #c3b3e6;
  --select-bg: #1d4453;
  --select-ink: #e7eeec;
  --scope-tint: #16262c;
  --scope-line: #33505c;
  --scope-line-strong: #48707f;
  --order-tint: #201d2b;
  --order-line: #453d5c;
  --order-line-strong: #5d5279;
  --backdrop: rgba(0, 0, 0, .62);
  --scrim: rgba(0, 0, 0, .55);
  --shadow-xs: 0 1px 2px rgba(0, 0, 0, .5);
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, .6);
  --shadow-md: 0 6px 20px rgba(0, 0, 0, .55);
  --shadow-lg: 0 24px 80px rgba(0, 0, 0, .7);
  --shadow-fab: 0 10px 30px rgba(0, 0, 0, .6);
  --shadow-drawer: 24px 0 60px rgba(0, 0, 0, .6);
  color-scheme: dark;
}

/* ===========================================================================
 * The painted themes.
 *
 * Five palettes and not one component rule between them. A theme here is a
 * block of token values; if a theme ever needs a rule of its own for some
 * control, that control is missing a token and the token is the fix. The whole
 * point of the audit at the top of this file is that the rule can be enforced.
 *
 * THE SELECTOR IS A PAIR, AND THE PAIR IS THE MECHANISM.
 *
 *   :root[data-theme="x"]        paints the page
 *   .swatch-face[data-theme="x"] paints the 18px preview in the picker
 *
 * One block serves both, so a preview cannot drift from the theme it advertises
 * and a new theme gets a correct swatch by existing. Nothing anywhere copies a
 * palette into a miniature.
 *
 * SPECIFICITY IS LOAD-BEARING HERE. `:root[data-theme="x"]` is (0,2,0), which
 * ties with the `:root:not([data-theme="light"])` guard in the dark media query
 * above, so these blocks must stay BELOW it or a visitor on a dark device would
 * get dark tokens over every painted theme. Do not move them up.
 *
 * EVERY VALUE IS DERIVED, AND EVERY VALUE IS A LITERAL. Both, on purpose.
 *
 * A theme is really eight seeds: a ground, a surface, an ink and the five
 * signal hues. The other twenty-odd tokens are mixes of those, and the recipe
 * is written beside each one as `mix: <a> <n>% + <b>`, meaning
 * `color-mix(in oklab, a n%, b)`. oklab rather than sRGB because a mix has to
 * behave the same on a turmeric and on a cyan, and sRGB interpolation goes
 * muddy through the middle. test/shop-themes.test.ts recomputes every recipe
 * and fails if a hex here has drifted from it, so the derivation is real and
 * enforced rather than a story about how the values were once chosen.
 *
 * The mixes are NOT left in the stylesheet, and that is a measurement, not a
 * preference. An unregistered custom property inherits as a stream of tokens
 * rather than as a value, so `background: var(--surface-3)` substitutes the
 * whole mix expression at every element that reads it and evaluates it there.
 * On a page holding sixty product rows, about four thousand elements, a theme
 * swap took 37 to 53ms of style recalculation with the mixes live, against
 * 0.9ms once the same palettes were written out. Two to three dropped frames
 * per swap, on the one interaction whose entire job is to look immediate.
 * Registering the tokens with `@property` was tried and did not help.
 *
 * So the recipe is the author's tool and the literal is what ships, and the
 * test is what stops those two from disagreeing.
 *
 * THE FIVE SIGNAL HUES DO NOT MOVE. `--accent` is blue-to-cyan, `--ok` green,
 * `--warn` amber, `--crit` red and `--gem` violet-to-magenta in every theme,
 * because those five are what the verdict badges and the risk bands are made
 * of and colour in this UI means the data said something. A theme is free to
 * repaint the room; it is not free to renumber the evidence.
 * ========================================================================= */

/*
 * Till Roll: thermal paper and carbon ink.
 *
 * The receipt is the original price record, and the one this shop hoped you
 * would throw away. Warm tan stock, a nearly black carbon ink that fades to
 * brown rather than to grey, and the blue of a rubber stamp for the accent.
 */
:root[data-theme="receipt"],
.swatch-face[data-theme="receipt"] {
  --ground: #e6dfd0;
  --surface: #f8f4ea;
  --ink: #1d1812;
  --accent: #2a4f86;
  --ok: #2b6a3e;
  --warn: #7c4f06;
  --crit: #a8322a;
  --gem: #67488a;
  --theme-color: #e6dfd0;
  --surface-2: #e9e5db;           /* mix: surface 94% + ink */
  --surface-3: #ddd9cf;           /* mix: surface 89% + ink */
  --ink-2: #4e4942;               /* mix: ink 74% + surface */
  --ink-3: #6c675f;               /* mix: ink 60% + surface */
  --line: #d1ccc3;                /* mix: surface 84% + ink */
  --line-strong: #a6a198;         /* mix: surface 66% + ink */
  --accent-soft: #d7dadc;         /* mix: accent 15% + surface */
  --ok-soft: #d7ddcd;             /* mix: ok 16% + surface */
  --warn-soft: #e5dac9;           /* mix: warn 15% + surface */
  --crit-soft: #efd5ca;           /* mix: crit 16% + surface */
  --gem-soft: #e1d9dc;            /* mix: gem 15% + surface */
  --link: #2a4f86;                /* = accent */
  --link-visited: #67488a;        /* = gem */
  --select-bg: #bbc4cf;           /* mix: accent 28% + surface */
  --select-ink: #1d1812;          /* = ink */
  --scope-tint: #e8e8e3;          /* mix: accent 7% + surface */
  --scope-line: #a1b0c3;          /* mix: accent 40% + surface */
  --scope-line-strong: #899cb7;   /* mix: accent 52% + surface */
  --order-tint: #ede7e4;          /* mix: gem 7% + surface */
  --order-line: #bbadc5;          /* mix: gem 40% + surface */
  --order-line-strong: #a998b9;   /* mix: gem 52% + surface */
  --backdrop: rgba(58, 48, 38, .58);
  --scrim: rgba(58, 48, 38, .48);
  --shadow: 0 8px 28px rgba(58, 48, 38, .34);
  --shadow-xs: 0 1px 2px rgba(58, 48, 38, .2);
  --shadow-sm: 0 1px 3px rgba(58, 48, 38, .32);
  --shadow-md: 0 6px 20px rgba(58, 48, 38, .32);
  --shadow-lg: 0 24px 80px rgba(58, 48, 38, .52);
  --shadow-fab: 0 10px 30px rgba(58, 48, 38, .44);
  --shadow-drawer: 24px 0 60px rgba(58, 48, 38, .44);
}

/*
 * Kanga: cream lettering on indigo cloth.
 *
 * A kanga is a printed claim somebody wears in public, which is what a verdict
 * badge is. Indigo ground, warm cream print rather than white, and the accent
 * taken from the cloth's own lighter blue so the five signal hues are still
 * the only saturated things on the page.
 */
:root[data-theme="kanga"],
.swatch-face[data-theme="kanga"] {
  --ground: #16223f;
  --surface: #1d2c51;
  --ink: #fdf5e4;
  --accent: #74b9ff;
  --ok: #59c98c;
  --warn: #f0b445;
  --crit: #ff968a;
  --gem: #c0a1f0;
  --theme-color: #16223f;
  --surface-2: #2a395b;           /* mix: surface 93% + ink */
  --surface-3: #324061;           /* mix: surface 89% + ink */
  --ink-2: #b5b5b4;               /* mix: ink 72% + ground */
  --ink-3: #979aa0;               /* mix: ink 60% + ground */
  --line: #344262;                /* mix: surface 88% + ink */
  --line-strong: #546079;         /* mix: surface 72% + ink */
  --accent-soft: #294065;         /* mix: accent 22% + ground */
  --ok-soft: #234451;             /* mix: ok 22% + ground */
  --warn-soft: #40414a;           /* mix: warn 22% + ground */
  --crit-soft: #3a364d;           /* mix: crit 17% + ground */
  --gem-soft: #383b62;            /* mix: gem 22% + ground */
  --link: #74b9ff;                /* = accent */
  --link-visited: #c0a1f0;        /* = gem */
  --select-bg: #33517b;           /* mix: accent 34% + ground */
  --select-ink: #fdf5e4;          /* = ink */
  --scope-tint: #273c66;          /* mix: accent 13% + surface */
  --scope-line: #3c5e8f;          /* mix: accent 38% + surface */
  --scope-line-strong: #43699d;   /* mix: accent 46% + surface */
  --order-tint: #303a64;          /* mix: gem 13% + surface */
  --order-line: #57568a;          /* mix: gem 38% + surface */
  --order-line-strong: #645f97;   /* mix: gem 46% + surface */
  --backdrop: rgba(5, 7, 15, .58);
  --scrim: rgba(5, 7, 15, .48);
  --shadow: 0 8px 28px rgba(5, 7, 15, .34);
  --shadow-xs: 0 1px 2px rgba(5, 7, 15, .2);
  --shadow-sm: 0 1px 3px rgba(5, 7, 15, .32);
  --shadow-md: 0 6px 20px rgba(5, 7, 15, .32);
  --shadow-lg: 0 24px 80px rgba(5, 7, 15, .52);
  --shadow-fab: 0 10px 30px rgba(5, 7, 15, .44);
  --shadow-drawer: 24px 0 60px rgba(5, 7, 15, .44);
}

/*
 * Phosphor: one green monitor in a dark room.
 *
 * The archive as a monitoring station. Ground and ink carry the whole identity
 * and the five signal hues stay where they are, which is a deliberate trade: a
 * true single-phosphor screen would be prettier and would leave "at 90d low"
 * and "at 90d high" the same colour, and this tool exists to tell those apart.
 */
:root[data-theme="phosphor"],
.swatch-face[data-theme="phosphor"] {
  --ground: #04120a;
  --surface: #08190f;
  --ink: #c6f5d4;
  --accent: #45d9e6;
  --ok: #4bf08c;
  --warn: #e8c04a;
  --crit: #ff6f5f;
  --gem: #b48cff;
  --theme-color: #04120a;
  --surface-2: #13251a;           /* mix: surface 93% + ink */
  --surface-3: #192d21;           /* mix: surface 89% + ink */
  --ink-2: #84a990;               /* mix: ink 70% + ground */
  --ink-3: #6b8c76;               /* mix: ink 58% + ground */
  --line: #1b2f22;                /* mix: surface 88% + ink */
  --line-strong: #364e3e;         /* mix: surface 72% + ink */
  --accent-soft: #0e342e;         /* mix: accent 20% + ground */
  --ok-soft: #103720;             /* mix: ok 20% + ground */
  --warn-soft: #2a3018;           /* mix: warn 20% + ground */
  --crit-soft: #312519;           /* mix: crit 20% + ground */
  --gem-soft: #212933;            /* mix: gem 20% + ground */
  --link: #45d9e6;                /* = accent */
  --link-visited: #b48cff;        /* = gem */
  --select-bg: #164a47;           /* mix: accent 32% + ground */
  --select-ink: #c6f5d4;          /* = ink */
  --scope-tint: #0f2c24;          /* mix: accent 12% + surface */
  --scope-line: #1d5753;          /* mix: accent 36% + surface */
  --scope-line-strong: #226664;   /* mix: accent 44% + surface */
  --order-tint: #1a2627;          /* mix: gem 12% + surface */
  --order-line: #40415c;          /* mix: gem 36% + surface */
  --order-line-strong: #4d4a6f;   /* mix: gem 44% + surface */
  --backdrop: rgba(0, 0, 0, .58);
  --scrim: rgba(0, 0, 0, .48);
  --shadow: 0 8px 28px rgba(0, 0, 0, .34);
  --shadow-xs: 0 1px 2px rgba(0, 0, 0, .2);
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, .32);
  --shadow-md: 0 6px 20px rgba(0, 0, 0, .32);
  --shadow-lg: 0 24px 80px rgba(0, 0, 0, .52);
  --shadow-fab: 0 10px 30px rgba(0, 0, 0, .44);
  --shadow-drawer: 24px 0 60px rgba(0, 0, 0, .44);
}

/*
 * Matatu: Nairobi livery.
 *
 * Hand-painted panels, chrome trim and neon tube, on a body colour that is
 * almost black. The loudest theme in the set, and the one that had to be
 * checked hardest: `--gem` takes the hot pink so the accent can stay in the
 * cyan the informational verdict needs, and the risk bands keep amber and red
 * to themselves.
 */
:root[data-theme="matatu"],
.swatch-face[data-theme="matatu"] {
  --ground: #100b1b;
  --surface: #191130;
  --ink: #f6efff;
  --accent: #35d8f5;
  --ok: #3ff08f;
  --warn: #ffc73d;
  --crit: #ff7d6f;
  --gem: #ff5fb0;
  --theme-color: #100b1b;
  --surface-2: #27203f;           /* mix: surface 92% + ink */
  --surface-3: #2d2644;           /* mix: surface 89% + ink */
  --ink-2: #a8a2b3;               /* mix: ink 70% + ground */
  --ink-3: #8b8597;               /* mix: ink 58% + ground */
  --line: #302a48;                /* mix: surface 87% + ink */
  --line-strong: #524c69;         /* mix: surface 70% + ink */
  --accent-soft: #1f2d40;         /* mix: accent 20% + ground */
  --ok-soft: #203232;             /* mix: ok 20% + ground */
  --warn-soft: #372c2a;           /* mix: warn 20% + ground */
  --crit-soft: #3c222e;           /* mix: crit 22% + ground */
  --gem-soft: #3d1d38;            /* mix: gem 22% + ground */
  --link: #35d8f5;                /* = accent */
  --link-visited: #ff5fb0;        /* = gem */
  --select-bg: #27485d;           /* mix: accent 34% + ground */
  --select-ink: #f6efff;          /* = ink */
  --scope-tint: #232948;          /* mix: accent 14% + surface */
  --scope-line: #305675;          /* mix: accent 38% + surface */
  --scope-line-strong: #346988;   /* mix: accent 48% + surface */
  --order-tint: #351c41;          /* mix: gem 14% + surface */
  --order-line: #682f5e;          /* mix: gem 38% + surface */
  --order-line-strong: #7f376b;   /* mix: gem 48% + surface */
  --backdrop: rgba(0, 0, 0, .58);
  --scrim: rgba(0, 0, 0, .48);
  --shadow: 0 8px 28px rgba(0, 0, 0, .34);
  --shadow-xs: 0 1px 2px rgba(0, 0, 0, .2);
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, .32);
  --shadow-md: 0 6px 20px rgba(0, 0, 0, .32);
  --shadow-lg: 0 24px 80px rgba(0, 0, 0, .52);
  --shadow-fab: 0 10px 30px rgba(0, 0, 0, .44);
  --shadow-drawer: 24px 0 60px rgba(0, 0, 0, .44);
}

/*
 * Newsprint: black on white, with one spot of red.
 *
 * The correction the seller never printed. Hard rules, true black ink and no
 * atmosphere at all, which also makes it the highest-contrast option in the
 * picker: the one to reach for on a phone in the sun, or with poor eyesight.
 */
:root[data-theme="newsprint"],
.swatch-face[data-theme="newsprint"] {
  --ground: #f1f0ec;
  --surface: #ffffff;
  --ink: #000000;
  --accent: #0b4ea2;
  --ok: #0a6b2e;
  --warn: #7a4a00;
  --crit: #b3170f;
  --gem: #4b2a7a;
  --theme-color: #f1f0ec;
  --surface-2: #eeeeee;           /* mix: surface 95% + ink */
  --surface-3: #d7d7d7;           /* mix: surface 88% + ink */
  --ink-2: #121212;               /* mix: ink 82% + surface */
  --ink-3: #424242;               /* mix: ink 62% + surface */
  --line: #b1b1b1;                /* mix: surface 76% + ink */
  --line-strong: #696969;         /* mix: surface 52% + ink */
  --accent-soft: #dfe8f4;         /* mix: accent 13% + surface */
  --ok-soft: #deeae0;             /* mix: ok 14% + surface */
  --warn-soft: #e8dfd5;           /* mix: warn 17% + surface */
  --crit-soft: #f9e3e0;           /* mix: crit 13% + surface */
  --gem-soft: #e5e2ee;            /* mix: gem 13% + surface */
  --link: #0b4ea2;                /* = accent */
  --link-visited: #4b2a7a;        /* = gem */
  --select-bg: #bfd0e9;           /* mix: accent 26% + surface */
  --select-ink: #000000;          /* = ink */
  --scope-tint: #f0f4fa;          /* mix: accent 6% + surface */
  --scope-line: #95b1d9;          /* mix: accent 44% + surface */
  --scope-line-strong: #799cce;   /* mix: accent 56% + surface */
  --order-tint: #f3f1f7;          /* mix: gem 6% + surface */
  --order-line: #aa9ec5;          /* mix: gem 44% + surface */
  --order-line-strong: #9485b5;   /* mix: gem 56% + surface */
  --backdrop: rgba(0, 0, 0, .58);
  --scrim: rgba(0, 0, 0, .48);
  --shadow: 0 8px 28px rgba(0, 0, 0, .34);
  --shadow-xs: 0 1px 2px rgba(0, 0, 0, .2);
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, .32);
  --shadow-md: 0 6px 20px rgba(0, 0, 0, .32);
  --shadow-lg: 0 24px 80px rgba(0, 0, 0, .52);
  --shadow-fab: 0 10px 30px rgba(0, 0, 0, .44);
  --shadow-drawer: 24px 0 60px rgba(0, 0, 0, .44);
}

/*
 * The light and dark previews, and the only place a palette is stated twice.
 *
 * Light lives on the bare `:root` and dark lives behind `:root`-scoped
 * selectors, so neither reaches an 18px span inside the picker. The face only
 * ever draws four of them, so four is what these carry rather than a copy of
 * either palette. If either theme's ground or signals move, these move too.
 */
.swatch-face[data-theme="light"] {
  --ground: #eef1f0;
  --accent: #14566e;
  --ok: #2a6b4a;
  --crit: #9b3a32;
  --line-strong: #b6c2be;
}

.swatch-face[data-theme="dark"] {
  --ground: #0c1112;
  --accent: #63b8d6;
  --ok: #57ae81;
  --crit: #e27a70;
  --line-strong: #3a4749;
}


/*
 * Which way each painted theme leans, for the controls the browser draws.
 *
 * Declared on the root only and never on a swatch: `color-scheme` on an 18px
 * span would repaint that span's own scrollbars and native widgets, and it has
 * neither. Without this the date pickers, select popups and overlay scrollbars
 * a browser draws itself stay light inside a dark theme.
 */
:root[data-theme="receipt"],
:root[data-theme="newsprint"] { color-scheme: light; }

:root[data-theme="kanga"],
:root[data-theme="phosphor"],
:root[data-theme="matatu"] { color-scheme: dark; }

* { box-sizing: border-box; }

/*
 * The two properties the view code writes inline, given a type.
 *
 * `--q` is a quality score and `--h` a bar height, both set per element from
 * JavaScript and both read inside a gradient or a `max()`. Untyped, a custom
 * property is a string: a malformed one is substituted verbatim, the whole
 * declaration is thrown out at computed-value time, and a bar with a bad score
 * loses its gradient rather than showing an empty one. Typed, a value that is
 * not a percentage never lands and the initial value stands.
 *
 * `inherits: false` matters more than it looks: without it every one of the
 * sixty rows would carry a copy down to each of its descendants. Registration
 * also makes both animatable, which nothing uses today, because a gradient
 * stop is a paint and this page does not animate paint.
 */
@property --q {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 0%;
}

@property --h {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 0%;
}


/*
 * The shell: a fixed frame with exactly one scrolling pane inside it.
 *
 * The document itself never scrolls. The bar, the listing controls and the
 * status line hold their place, and the products scroll on their own, so the
 * search box and the count are still on screen at row four hundred. A page
 * where everything scrolls together puts the controls out of reach exactly
 * when a long result makes them worth reaching for.
 */
body {
  margin: 0;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 13px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
}

button, input, select { font: inherit; color: inherit; }

/*
 * `hidden` only works because the user-agent stylesheet says
 * `[hidden] { display: none }`, and author rules outrank the UA origin no
 * matter how weak their selector. So any class here that sets a `display`
 * silently defeats the attribute: `.banner` set `display: flex` and stayed
 * pinned over the viewport as an empty fixed box, eating clicks, however
 * many times the script set `banner.hidden = true`. Restating the rule at
 * author level with `!important` puts the attribute back in charge for every
 * element, present and future.
 */
[hidden] { display: none !important; }

/*
 * Every animation on this page is opt-out in one place.
 *
 * There is deliberately nothing here about view transitions. A cross-fade on
 * the theme swap was built and then removed: measured on a page holding sixty
 * product rows, the plain swap cost 40ms of main-thread work and the same swap
 * inside a view transition cost 77ms to reach `ready`, before any animation
 * began. Doubling the cost of the one interaction whose job is to feel
 * immediate is not a trade worth making on a mid-range phone, so the swap is
 * now a single attribute write with nothing wrapped around it.
 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/*
 * The frame a theme lands on, with every transition held still.
 *
 * /lib/theme.js sets this attribute, then lifts it two frames later. It exists
 * because a transition fires on ANY change to the value it watches, including
 * the one a new palette makes: without it the controls that fade under a
 * pointer would spend 120ms fading to the new theme while the table beside
 * them changed instantly, and the page would arrive in two parts.
 *
 * A theme is a state, not a movement. Nothing here should be seen travelling.
 */
:root[data-swapping] *,
:root[data-swapping] *::before,
:root[data-swapping] *::after { transition: none !important; }

/* ---------- links, selection, focus ---------- */

/*
 * Every link, in both themes, distinguished from body text by colour AND by
 * an underline on hover. Colour alone fails anyone who cannot separate the
 * two hues, and this page prints brand names and category names beside links
 * constantly.
 *
 * The product-name anchors in the table are the deliberate exception and set
 * their own colour: sixty tinted links down a dense grid is a texture, not a
 * signal, and the whole row is clickable anyway.
 */
a { color: var(--link); text-decoration: none; }
a:visited { color: var(--link-visited); }
a:hover { text-decoration: underline; text-underline-offset: 2px; }
a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }

/*
 * Selection is themed too. The browser default is a light blue that is
 * unreadable over a dark surface, which is exactly the case where someone is
 * selecting a price to copy it.
 */
::selection { background: var(--select-bg); color: var(--select-ink); }

/* Everything focusable answers the keyboard, not only the controls that were
 * remembered one at a time. */
:focus-visible { outline-color: var(--accent); }

/* ---------- top bar ---------- */

.topbar {
  position: relative; z-index: 20;
  flex: none;
  min-height: var(--topbar);
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px 14px;
  padding: 0 14px;
  background: var(--surface);
  border-bottom: 1px solid var(--line-strong);
}

/*
 * Nothing in the bar wears a box.
 *
 * Every control here used to sit in its own bordered pill, so a bar holding a
 * brand, a search field and three controls read as five separate widgets on
 * one strip. The search box is the only thing with an outline now; the rest
 * are text that lights on hover, which is what makes the bar look like a bar.
 */
.topbtn {
  appearance: none; border: 0; background: transparent; color: var(--ink-2);
  padding: 5px 9px; border-radius: 5px; cursor: pointer;
  font: inherit; font-size: 12.5px; font-weight: 560;
  transition: color .12s, background .12s;
}
.topbtn:hover { background: var(--surface-3); color: var(--ink); }
.topbtn:active { color: var(--accent); }
.topbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/*
 * The switches, in a dropdown, at every width.
 *
 * They used to be laid out inline on a desktop and folded into a menu only
 * below 640px. The bar is now brand, search and one button at every size, and
 * everything that is a SETTING rather than a query lives behind that button.
 *
 * The reason is what this page is. It is a dense table for comparing prices,
 * and the bar is the frame around it: a control that changes how the page
 * LOOKS competes for attention with the search box, which is the only control
 * most visits use. A theme picker is the most decorative thing here and was
 * about to become the widest, which is the wrong trade on a strip that has to
 * stay quiet.
 *
 * One consequence worth stating, because it is the whole reason this shape is
 * safe: there is still exactly one copy of every control. The menu is the same
 * DOM at 375 and at 1280, so there is no second switch to keep in step and no
 * width at which a setting is unreachable.
 *
 * The panel is positioned, so opening it cannot reflow the bar behind it.
 */
.topmenu {
  display: none;
  position: absolute; top: 100%; right: 8px; z-index: 30;
  flex-direction: column; align-items: stretch; gap: 3px;
  width: min(300px, calc(100vw - 16px));
  /* Short screens and long lists: the panel scrolls rather than running off
     the bottom, and the bar it hangs from never moves. */
  max-height: min(560px, calc(100dvh - var(--topbar) - 16px));
  overflow-y: auto; overscroll-behavior: contain;
  padding: 7px;
  background: var(--surface);
  border: 1px solid var(--line-strong); border-radius: 10px;
  box-shadow: var(--shadow-md);
}
.topbar.menu-open .topmenu { display: flex; }
/* The slot takes the middle and gives the rest back, which is what holds the
   menu button against the right edge. */
.searchslot { order: 1; flex: 1; min-width: 0; max-width: 560px; margin-right: auto; }
.menubtn { order: 5; }

/*
 * Inside the panel every control is a full-width row with its words on the
 * left, which is what a menu is. Alerts and About keep the borderless
 * treatment they had in the bar; they simply stop being centred.
 */
.topmenu .topbtn { text-align: left; padding: 9px 10px; }
/* An anchor now, so it must decline the link styling above: the brand is ink
   in both states and never underlines, because it reads as a wordmark first
   and a control second. */
.brand { display: flex; align-items: center; gap: 9px; font-weight: 640; letter-spacing: -.01em; white-space: nowrap; }
a.brand, a.brand:visited { color: var(--ink); text-decoration: none; }
a.brand:hover { text-decoration: none; color: var(--accent); }
.brand .mark {
  width: 10px; height: 10px; border-radius: 2px;
  background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}

/*
 * The theme picker: eight named rows inside the menu.
 *
 * A list rather than a strip of swatches, and every row says what it is in
 * words. Most of this audience is on a phone, where there is no hover and a
 * tooltip is a control nobody can reach, so a row of coloured dots that only
 * explain themselves on a pointer would be eight unlabelled buttons. The face
 * shows the palette, the name says which one it is, and the line under it says
 * what it is for. Nothing here needs to be discovered.
 *
 * Every option is visible at once, which is the other half of the point: a
 * select would answer "what does that one look like" one option at a time,
 * and comparing is the whole activity.
 */
.themes {
  display: flex; flex-direction: column; gap: 1px;
  margin-top: 4px; padding-top: 6px;
  border-top: 1px solid var(--line);
}

/* A label for the section, because the rows below it are not self-evidently a
   group once Alerts and About are sitting above them. */
.themes::before {
  content: 'Theme';
  padding: 2px 10px 5px;
  font-size: 10px; font-weight: 620; letter-spacing: .07em; text-transform: uppercase;
  color: var(--ink-3);
}

.swatch {
  appearance: none; border: 0; background: transparent; color: var(--ink-2);
  display: grid; grid-template-columns: auto 1fr; gap: 1px 10px; align-items: center;
  width: 100%; text-align: left;
  padding: 7px 10px; border-radius: 7px; cursor: pointer;
  font: inherit;
  transition: background .12s, color .12s;
}
.swatch:hover { background: var(--surface-3); color: var(--ink); }
.swatch:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }

/*
 * The face: a quarter-turn of the palette the row selects.
 *
 * Half the disc is the ground and the other half is the three verdict hues in
 * the order the table uses them, so the preview answers the question that
 * actually matters about a theme here: can you still tell an "at 90d low" from
 * an "at 90d high" in it. Drawn with a conic gradient rather than four child
 * spans, because four spans is four more nodes per row and a gradient is one
 * paint. It spans both text rows, so it reads as the row's marker.
 */
.swatch-face {
  grid-row: 1 / 3;
  display: block; width: 20px; height: 20px; border-radius: 6px;
  border: 1px solid var(--line-strong);
  background: conic-gradient(from 135deg,
    var(--ground) 0 50%,
    var(--accent) 50% 67%,
    var(--ok) 67% 84%,
    var(--crit) 84% 100%);
  /* Transform only, so the lift is a compositor job and never a reflow of the
     list it sits in. */
  transition: transform .14s ease;
}

/* System has no palette, so it shows the two answers the device chooses
   between rather than pretending to be one of them. */
.swatch-face:not([data-theme]) {
  background: linear-gradient(135deg, var(--face-day) 0 50%, var(--face-night) 50% 100%);
}

.swatch:hover .swatch-face,
.swatch:focus-visible .swatch-face { transform: scale(1.1); }

/*
 * Which one you are wearing, said three ways: a ring on the face, the accent
 * on the name, and the check at the end of the row. Three, because colour
 * alone is a signal some readers do not get, and this is the one row in the
 * list whose state a person is actually looking for.
 *
 * The ring is drawn in the PAGE's accent, not the row's, so it is stated in
 * the theme currently in force.
 */
.swatch.is-active .swatch-face {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 3.5px var(--accent);
}
.swatch.is-active { background: var(--surface-2); }
.swatch.is-active .swatch-name { color: var(--accent); }
.swatch.is-active .swatch-name::after {
  content: '✓';
  margin-left: 6px;
  font-weight: 700;
}

.swatch-name {
  grid-column: 2;
  font-size: 12.5px; font-weight: 560; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/*
 * The description, always visible and never a tooltip.
 *
 * It is what makes the list readable by somebody who has never seen this page
 * before, which is most of the people who will see it. Two lines at most at
 * the panel's width, and it wraps rather than truncating: a description cut
 * off mid-sentence is worse than no description.
 */
.swatch-note {
  grid-column: 2;
  font-size: 10.5px; line-height: 1.4; font-weight: 400; color: var(--ink-3);
}
.swatch:hover .swatch-note { color: var(--ink-2); }

/*
 * The bar's one control: three bars, drawn rather than typed.
 *
 * It was "···", which is the glyph for "more of this row" and read as a
 * continuation of the brand beside it. Three bars is the shape everyone
 * already knows, and one span with two shadows needs no image and no font.
 *
 * It used to appear only below 640px. It is the way in to every setting now,
 * so it is present at every width.
 */
.menubtn {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 30px; padding: 0;
  border: 0; border-radius: 5px;
  background: transparent; color: var(--ink-2); cursor: pointer;
}
.menubtn:hover { background: var(--surface-3); color: var(--ink); }
.menubtn-bars {
  display: block; width: 17px; height: 2px; border-radius: 2px;
  background: currentColor;
  box-shadow: 0 -5px 0 currentColor, 0 5px 0 currentColor;
}
.menubtn[aria-expanded="true"] { background: var(--surface-3); color: var(--accent); }

/* ---------- layout ---------- */

/*
 * The row between the bar and the status line, and the only part of the page
 * with height to give away. `min-height: 0` is what lets the pane inside it
 * scroll: without it a flex child refuses to shrink below its content and the
 * overflow escapes to the document, which is the layout this replaces.
 */
.view { flex: 1; min-height: 0; display: flex; }
.shop { flex: 1; display: flex; min-width: 0; }

/* ---------- search ---------- */

/*
 * The search box sits in the top bar, in the slot the shell keeps for it.
 *
 * It was the first field inside the filter rail, which on a phone put the most
 * used control in the shop two taps deep inside a drawer of twenty others.
 */
.searchwrap { position: relative; display: block; }
.searchbox {
  width: 100%; padding: 7px 30px 7px 11px;
  border: 1px solid var(--line-strong); border-radius: 7px;
  background: var(--surface-2); color: var(--ink); font-size: 13px;
}
.searchbox::-webkit-search-cancel-button { display: none; }
.searchbox:hover { border-color: var(--ink-3); }
.searchbox:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.searchclear {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  width: 22px; height: 22px; padding: 0;
  border: 0; border-radius: 999px; background: transparent;
  color: var(--ink-3); font-size: 15px; line-height: 1; cursor: pointer;
}
.searchclear:hover { background: var(--surface-3); color: var(--ink); }

/* ---------- filter rail ---------- */

/* Its own scrollbar, and its own height. The shell holds the row still, so the
   rail scrolls the filters without the table moving under it. */
.rail {
  flex: none; width: var(--rail);
  overflow-y: auto;
  background: var(--surface);
  border-right: 1px solid var(--line);
  display: flex; flex-direction: column;
}
.rail-backdrop { display: none; }
.rail-head {
  position: sticky; top: 0; z-index: 2;
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px 10px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.rail-head h2 {
  margin: 0; flex: 1;
  font-family: var(--mono); font-size: 10px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-3); font-weight: 600;
}
.railreset {
  border: 0; background: none; padding: 0; cursor: pointer;
  color: var(--accent); font-size: 11.5px; text-decoration: underline;
}
.railclose {
  display: none;
  border: 0; background: none; cursor: pointer;
  font-size: 19px; line-height: 1; color: var(--ink-3); padding: 0 2px;
}
.rail-body { display: flex; flex-direction: column; gap: 15px; padding: 14px; }

.rail h3 {
  margin: 0 0 5px; font-family: var(--mono); font-size: 9.5px;
  letter-spacing: .13em; text-transform: uppercase; color: var(--ink-3); font-weight: 600;
}
.field { display: flex; flex-direction: column; gap: 5px; }
/*
 * Scoped away from checkboxes on purpose. This rule is for the text and number
 * fields, but the checkboxes sit inside `.field` too, so an unqualified
 * `.field input` stretched them to `width: 100%`, 135px of box around a 13px
 * tick, which the flex row then shrank by different amounts per label, leaving
 * the two boxes misaligned and their text wrapping.
 */
.field input:not([type="checkbox"]), .field select {
  width: 100%; padding: 6px 8px; border: 1px solid var(--line);
  border-radius: 5px; background: var(--surface-2); font-size: 12.5px;
  transition: border-color .12s, background .12s;
}
.field input:hover:not([type="checkbox"]), .field select:hover { border-color: var(--line-strong); }
.field input:focus, .field select:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.pair { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.check { display: flex; align-items: center; gap: 7px; cursor: pointer; color: var(--ink-2); font-size: 12.5px; }
/* `flex: none` so the tick keeps its size whatever the label beside it does. */
.check input[type="checkbox"] {
  flex: none; width: 14px; height: 14px; margin: 0; accent-color: var(--accent);
}
/* On a selected row the total takes the row's own colour; the muted token is
 * for a resting one and disappears against the accent. */
.multi-row.is-on .of { color: inherit; }
.linkbtn {
  background: none; border: 0; color: var(--accent); cursor: pointer;
  padding: 0; font-size: 12px; text-align: left; text-decoration: underline;
}

.hint { margin: 3px 0 0; font-size: 10.5px; line-height: 1.4; color: var(--ink-3); }

/* ---------- multi-select ---------- */

/*
 * A checkable list with its own search box, rather than a native multiple
 * select. Four hundred categories in a native control is a scrolling wall with
 * no way to find one and no visible record of what is already chosen. Chosen
 * rows are pinned to the top by the view, so the list scrolling away never
 * hides the current selection.
 */
.multi {
  border: 1px solid var(--line); border-radius: 5px;
  background: var(--surface-2); overflow: hidden;
}
.multi-search {
  width: 100%; border: 0; border-bottom: 1px solid var(--line);
  background: transparent; padding: 5px 8px; font-size: 12px;
}
.multi-search:focus { outline: 2px solid var(--accent); outline-offset: -2px; }
.multi-list { max-height: 168px; overflow-y: auto; scrollbar-width: thin; }
.multi-row {
  display: flex; align-items: center; gap: 7px;
  /* The extra bottom padding is where the total hangs. It buys a few pixels
   * of row height and costs nothing in RAIL WIDTH, which is the dimension
   * that would have pushed the table sideways. */
  padding: 3px 8px 10px; cursor: pointer; font-size: 12px;
}
.multi-row:hover { background: var(--surface-3); }
.multi-row.is-on { background: var(--accent-soft); color: var(--accent); font-weight: 560; }
.multi-row input[type="checkbox"] {
  flex: none; width: 13px; height: 13px; margin: 0; accent-color: var(--accent);
}
.multi-pick { display: flex; align-items: center; gap: 7px; flex: 1; min-width: 0; cursor: pointer; }
.multi-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/*
 * The exclude control: one round minus per row.
 *
 * On a brand list the useful sentence is "anything but these three", and
 * including four hundred brands one at a time is not an answer to it. The
 * button is faint until the row is hovered so four hundred of them do not
 * read as four hundred warnings; on a touch screen there is no hover, so it
 * is always visible there.
 */
.multi-minus {
  flex: none; width: 17px; height: 17px; padding: 0;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--surface); color: var(--ink-3);
  font-size: 12px; line-height: 1; cursor: pointer;
  opacity: 0; transition: opacity .12s, color .12s, border-color .12s;
}
.multi-row:hover .multi-minus, .multi-minus:focus-visible { opacity: 1; }
.multi-minus:hover { border-color: var(--crit); color: var(--crit); }
.multi-minus.is-on {
  opacity: 1; border-color: var(--crit); background: var(--crit-soft); color: var(--crit);
}
@media (hover: none) {
  .multi-minus { opacity: 1; }
}
.multi-row.is-out { background: var(--crit-soft); }
.multi-row.is-out .multi-pick .multi-name {
  color: var(--crit); text-decoration: line-through; text-decoration-color: var(--crit);
}

/* What is excluded, said in words under the list. A disabled row three
   hundred entries down is not a record anybody can find again. */
.multi-chips { display: flex; flex-wrap: wrap; gap: 4px; padding: 5px 6px 0; }
.multi-chips:empty { display: none; }
.xchip {
  display: inline-flex; align-items: center; gap: 5px;
  border: 1px solid var(--crit); background: var(--crit-soft); color: var(--crit);
  border-radius: 999px; padding: 1px 8px; font-size: 10.5px; cursor: pointer;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.xchip:hover { background: var(--surface-3); }
.xchip-x { font-size: 12px; line-height: 1; }
/*
 * A fixed-width numeric column, so four hundred brand rows line up on their
 * last digit and the rail never changes width because a count grew a digit.
 */
.multi-n {
  flex: none; width: 54px; text-align: right;
  font-family: var(--mono); font-size: 10px; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.multi-more {
  padding: 6px 8px; border-top: 1px solid var(--line);
  font-size: 10.5px; color: var(--ink-3);
}
.multi-none { padding: 8px; font-size: 11.5px; color: var(--ink-3); }
.multi-clear {
  width: 100%; border: 0; border-top: 1px solid var(--line);
  background: transparent; color: var(--accent); cursor: pointer;
  padding: 4px; font-size: 11px; text-align: center;
}
.multi-clear:hover { background: var(--surface-3); }

/* ---------- sliders ---------- */

.slider-row { display: flex; align-items: center; gap: 9px; }
.slider {
  flex: 1; min-width: 0; appearance: none; height: 4px; border-radius: 999px;
  background: linear-gradient(90deg, var(--ok), var(--warn) 60%, var(--crit));
  cursor: pointer;
}
.slider::-webkit-slider-thumb {
  appearance: none; width: 14px; height: 14px; border-radius: 50%;
  background: var(--surface); border: 2px solid var(--ink);
  box-shadow: var(--shadow-sm); cursor: grab;
}
.slider::-moz-range-thumb {
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--surface); border: 2px solid var(--ink); cursor: grab;
}
.slider:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
.slider-v {
  flex: none; min-width: 82px; text-align: right;
  font-family: var(--mono); font-size: 10.5px; color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}

/* ---------- results ---------- */

/*
 * Two rows: the controls, which never move, and the listing, which is the one
 * scrolling box on the page. The pill strip of twelve collections that used to
 * head this column is gone; the collection select in the bar below is the one
 * control, at every width.
 */
.results { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.resbar {
  flex: none;
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 10px 14px 8px; border-bottom: 1px solid var(--line);
  font-family: var(--mono); font-size: 11px; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
/* The scrolling pane. `min-height: 0` for the same reason .view needs it. */
.listing { flex: 1; min-height: 0; overflow-y: auto; padding: 6px 14px 24px; }
/*
 * The count and the total it came out of, in one shape used everywhere.
 *
 * The total is absolutely positioned under the baseline, so it adds no line
 * height and cannot push a table row or a rail row taller. It is always
 * visible rather than a tooltip: a phone has no hover, and this is read on a
 * phone.
 */
.count {
  position: relative; display: inline-block; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.count .count-n { font-weight: 600; }
.count .count-label { font-weight: 400; }
.count .of {
  position: absolute; right: 0; top: 100%; margin-top: -2px;
  font-size: 8.5px; line-height: 1; letter-spacing: .02em; font-weight: 400;
  color: var(--ink-3); pointer-events: none;
}
.resbar .count { color: var(--ink); font-size: 12px; font-weight: 600; }
.resbar .count .count-label { color: var(--ink-3); }

/*
 * One shape for every control in this bar.
 *
 * The sort box, the direction flip, the collection box and the Filters button
 * are four controls of equal standing, and they read as one row of controls
 * only if they agree on height, border, radius and type size. They did not,
 * and the bar looked like three unrelated widgets that happened to be near
 * each other.
 */
.ctl {
  height: 26px; padding: 0 8px;
  border: 1px solid var(--line); border-radius: 5px;
  background: var(--surface); color: var(--ink-2);
  font-family: var(--sans); font-size: 11.5px;
  cursor: pointer;
}
.ctl:hover:not(:disabled) { border-color: var(--line-strong); color: var(--ink); }
/* A control that cannot act says so rather than accepting the click and doing
   nothing: the direction flip is inert while the order is a shuffle. */
.ctl:disabled { opacity: .5; cursor: default; }
/*
 * No viewport cap any more. The flex rules below are what bound these boxes,
 * and `min-width: 0` is what stops a long option name from setting a floor, so
 * a vw cap can only fight them: at 375px a 46vw ceiling landed two pixels under
 * the width flex had just worked out, and every wrapped row came out ragged.
 */
.resbar select.ctl { padding: 0 4px; max-width: 100%; }
.sortsel { border-radius: 5px 0 0 5px; }
.sortwrap { display: inline-flex; align-items: stretch; }
.dirbtn {
  border-left: 0; border-radius: 0 5px 5px 0;
  font-family: var(--mono); font-size: 12px;
}
.filtersbtn { display: none; align-items: center; gap: 6px; }
.filtersbtn.has-filters::after {
  content: attr(data-count);
  font-family: var(--mono); font-size: 10px; font-weight: 600;
  background: var(--accent); color: var(--surface);
  border-radius: 999px; min-width: 15px; height: 15px;
  display: inline-flex; align-items: center; justify-content: center; padding: 0 4px;
}
.clearbtn { color: var(--accent); border-color: var(--accent); }
.clearbtn:hover { background: var(--accent-soft); color: var(--accent); }

/*
 * The bar spends every pixel it is given, and wraps into rows that line up.
 *
 * One basis shared by all three growing controls, rather than a basis each:
 * equal bases are what makes a second row align with the first instead of
 * stepping, which is the whole of "uniform wrap widths". The flip and the two
 * buttons are content-sized, because a 26px arrow stretched to a quarter of the
 * bar is a target that grew for no reason, and because a bar of four equals
 * reads as four choices when it is three choices and a verb.
 *
 * `min-width: 0` is load-bearing on the selects: a flex item's floor is its
 * min-content width, and for a select that is its longest option, so without
 * it the row overflows the bar instead of wrapping inside it.
 */
.resbar > .ctl, .resbar > .sortwrap { flex: 1 1 160px; min-width: 0; }
/* Declared after the rule above, same specificity, so source order is what
   makes the content-sized controls win. */
.resbar > .filtersbtn, .resbar > .clearbtn { flex: 0 0 auto; }
/* The reading-tier note is a caption, not a control, so it takes a row of its
   own rather than a share of the row the controls are wrapping into. */
.resbar > .tier { flex: 1 1 100%; }
.sortwrap > .sortsel { flex: 1 1 auto; min-width: 0; }
.sortwrap > .dirbtn { flex: 0 0 auto; }

/*
 * The colour code, applied.
 *
 * Scope is every control that decides WHICH products are in the result: the
 * collection box, the Filters door, and every box in the rail behind it. Order
 * is the sort box and the direction flip. The view box carries neither, and
 * neither does Clear, which is a verb rather than a chooser and already owns
 * the accent outline that says so.
 *
 * These sit after `.ctl` and after `.clearbtn` on purpose. All three are one
 * class deep, so source order is the only thing deciding them, and the hover
 * pair has to restate `:hover:not(:disabled)` to reach the same specificity as
 * the neutral hover above rather than lose the keyline the moment a pointer
 * lands on it.
 */
.ctl-scope { background: var(--scope-tint); border-color: var(--scope-line); }
.ctl-scope:hover:not(:disabled) { border-color: var(--scope-line-strong); }
.ctl-order { background: var(--order-tint); border-color: var(--order-line); }
.ctl-order:hover:not(:disabled) { border-color: var(--order-line-strong); }

/*
 * The rail is scope, all of it, so its boxes take the tint rather than each
 * field being marked one at a time in the view code. Every selector here is
 * one class deeper than the neutral rule it replaces, so it wins on
 * specificity and does not depend on where in the file it sits.
 */
.rail .multi { background: var(--scope-tint); border-color: var(--scope-line); }
.rail .multi-search { border-bottom-color: var(--scope-line); }
.rail .field input:not([type="checkbox"]), .rail .field select {
  background: var(--scope-tint); border-color: var(--scope-line);
}
.rail .field input:hover:not([type="checkbox"]), .rail .field select:hover {
  border-color: var(--scope-line-strong);
}

/* How the search was read. The loosened readings get a warmer tint so the
   eye notices the result is not what was typed. */
.resbar .tier {
  font-family: var(--mono); font-size: 10.5px; border-radius: 999px; padding: 0 7px;
  border: 1px solid var(--line); color: var(--ink-3);
}
.resbar .tier-phrase, .resbar .tier-words, .resbar .tier-loose, .resbar .tier-dropped {
  border-color: var(--warn); color: var(--warn);
}

.tablewrap { animation: settle .18s ease; }
@keyframes settle { from { opacity: .4; } }

table.grid { width: 100%; border-collapse: collapse; }
table.grid th {
  /* Sticky to the listing, which is the scroll container now, so the header
     stops at the top of the pane rather than under the bar. */
  position: sticky; top: 0; z-index: 5;
  text-align: left; white-space: nowrap;
  font-family: var(--mono); font-size: 9.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-3); font-weight: 600;
  /*
   * Eight pixels, not ten, and nine on the numeric columns rather than
   * fourteen. Eleven columns multiply a padding: the wider thumbnail spent
   * 22px of the row and the gutters were where it had to come from, or the
   * table stops fitting its column at 1280 and drags the whole page sideways.
   */
  padding: 6px 8px 6px 0; background: var(--ground);
  border-bottom: 1px solid var(--line-strong);
  cursor: pointer; user-select: none;
}
table.grid th:hover { color: var(--ink); }
table.grid th.sorted { color: var(--accent); }
/* The resting glyph is faint and appears on hover; the live arrow is solid.
   Price keeps its glyph visible at rest because it is the sort people look for. */
table.grid th .dir { margin-left: 4px; opacity: .35; transition: opacity .12s; }
table.grid th.sortable:hover .dir, table.grid th.sorted .dir, table.grid th.sortable-key .dir { opacity: 1; }
table.grid th.sortable:hover { text-decoration: underline; text-decoration-color: var(--line-strong); text-underline-offset: 3px; }
table.grid th.sortable-key { color: var(--ink-2); }
table.grid th:not(.sortable) { cursor: default; }
table.grid th.sortable:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
table.grid th.r, table.grid td.r { text-align: right; padding-right: 9px; }

table.grid td {
  padding: 3px 8px 3px 0; border-bottom: 1px solid var(--line);
  vertical-align: middle; font-size: 12px;
}
table.grid tr.row { cursor: pointer; }
table.grid tr.row:hover td { background: var(--surface-2); }
table.grid tr.row:hover td.name a { text-decoration: underline; text-underline-offset: 2px; }
.multi-row.is-empty .multi-name { color: var(--ink-3); text-decoration: line-through; text-decoration-color: var(--line-strong); }
table.grid tr.row.is-cursor td { background: var(--accent-soft); }
td.num, td.r { font-family: var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
td.name { min-width: 0; }
td.name a { color: var(--ink); text-decoration: none; font-weight: 560; }
td.name a:hover { color: var(--accent); text-decoration: underline; }
td.meta { color: var(--ink-3); font-size: 11px; white-space: nowrap; }
td.save { color: var(--ok); }
.sub { display: block; font-size: 10.5px; color: var(--ink-3); font-weight: 400; }
.sub.save { color: var(--ok); font-family: var(--mono); }
/* Only-on-small helper. Declared after .sub on purpose: same specificity, so
 * source order is what makes it win on desktop, and the 640px media block
 * further down is what turns it back on. */
.m-only { display: none; }

/*
 * The thumbnail rides inside the row's single line and grows on hover instead
 * of costing height: the cell keeps its 36px box while the image scales out
 * over the table, so density and legibility stop being a trade.
 * The cell's left edge carries the evidence tone, which is what keeps the
 * verdict visible on a phone after the Evidence column is hidden.
 */
td.thumb {
  width: 66px; padding: 2px 8px 2px 0;
  border-left: 3px solid transparent;
}
td.thumb.ev-low { border-left-color: var(--ok); }
td.thumb.ev-below { border-left-color: var(--accent); }
td.thumb.ev-high { border-left-color: var(--warn); }
td.thumb.ev-unproven { border-left-color: var(--line); }
.thumbbtn {
  position: relative; z-index: 1;
  display: block; width: 56px; line-height: 0;
  margin-left: 4px; padding: 0; border: 0; background: none; cursor: pointer;
}
/*
 * Contain, not cover. A product photo cropped to a box loses the thing being
 * sold: a cable coiled in the middle of a white square becomes a white square.
 * Height follows the image and is capped, so the row grows by a few pixels at
 * most and every thumbnail still lines up on its left edge.
 */
/* The min-height is for the image that never arrives: with an auto height a
   broken CDN url collapses the cell and the row loses half its height. */
.thumbbtn img, .thumbbtn .noimg {
  display: block; width: 56px; height: auto; min-height: 36px; max-height: 56px;
  object-fit: contain; border-radius: 5px;
  background: var(--image-pad); border: 1px solid var(--line);
  transition: transform .16s ease, box-shadow .16s ease, border-radius .16s ease;
  transform-origin: left center;
}
.thumbbtn .noimg { height: 44px; background: var(--surface-3); }
.thumbbtn:hover, .thumbbtn:focus-visible { z-index: 30; }
.thumbbtn:hover img, .thumbbtn:focus-visible img {
  transform: scale(2.6); border-radius: 4px; box-shadow: var(--shadow);
}

/*
 * The product code, as a control.
 *
 * Mono and muted so it reads as an identifier rather than as part of the
 * title, and a real button because tapping it copies. It must not open the
 * dialog, which is why it is a sibling of the title link rather than inside it.
 */
.pcode {
  border: 0; background: none; padding: 0 5px 0 0; cursor: pointer;
  font-family: var(--mono); font-size: 10.5px; color: var(--ink-3);
  vertical-align: baseline;
}
.pcode:hover { color: var(--accent); text-decoration: underline; }

/* Out of stock, and only ever visible when the visitor asked for it. */
tr.row.is-out, .pcard.is-out { opacity: .55; }
.badge-out { background: var(--surface-3); color: var(--crit); }

/* Badge form of the evidence verdict; scoped under .ev so the same modifier
 * names can tint other elements (the thumb rail, the dialog) differently. */
.ev {
  display: inline-block; font-family: var(--mono); font-size: 10px;
  letter-spacing: .04em; padding: 2px 7px; border-radius: 999px; white-space: nowrap;
}
.ev.ev-low { background: var(--ok-soft); color: var(--ok); font-weight: 600; }
.ev.ev-below { background: var(--accent-soft); color: var(--accent); }
.ev.ev-high { background: var(--warn-soft); color: var(--warn); }
.ev.ev-unproven { background: var(--surface-3); color: var(--ink-3); }

.claimed { color: var(--ink-3); }
.true-pos { color: var(--ok); font-weight: 600; }

/*
 * A claimed discount the price history contradicts is struck through rather
 * than hidden. Hiding it would leave the reader wondering what the merchant
 * said; striking it says we checked, and it was not true.
 */
td.claimed.is-inflated { color: var(--crit); text-decoration: line-through; opacity: .85; }

/* ---------- quality, risk and row badges ---------- */

/*
 * The quality column reads as a shape, not as eleven two-digit numbers. The
 * bar is drawn behind the figure with a gradient stop, so scanning the column
 * finds the good rows without reading any of them.
 */
td.quality { padding-right: 10px; }
.qbar {
  display: inline-flex; align-items: center; justify-content: flex-end;
  min-width: 46px; padding: 1px 5px; border-radius: 3px;
  font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 11.5px;
  background: linear-gradient(to right,
    var(--accent-soft) 0 var(--q, 0%), transparent var(--q, 0%) 100%);
  border: 1px solid var(--line);
}
.qbar b { font-weight: 600; }

/*
 * The badges wrap, and that is a layout fix rather than a style choice.
 *
 * An inline-flex row that cannot wrap contributes the SUM of its children to
 * the name column's minimum width, so three badges made that column 200px
 * wide at every size and the nine-column table stopped fitting its own area
 * between 900 and 1200px, dragging the whole document sideways. Wrapping makes
 * the minimum the widest single badge instead.
 */
.badges { display: inline-flex; flex-wrap: wrap; gap: 4px; margin-left: 6px; vertical-align: baseline; }
.badge {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: .03em;
  padding: 1px 5px; border-radius: 3px; white-space: nowrap; font-weight: 600;
}
.badge-multi { background: var(--gem-soft); color: var(--gem); }
.badge-q { background: var(--accent-soft); color: var(--accent); }
.badge-refurb { background: var(--surface-3); color: var(--ink-3); }
.badge-durable { background: var(--ok-soft); color: var(--ok); }
/* Risk badges appear only above a threshold, so a badge always means
 * something worth looking at rather than decorating every row. */
.risk-low { background: var(--surface-3); color: var(--warn); }
.risk-mid { background: var(--warn-soft); color: var(--warn); }
.risk-high { background: var(--crit-soft); color: var(--crit); }

/* The infinite-scroll sentinel and what it says at each stage. */
.sentinel { min-height: 48px; padding: 14px 0 24px; text-align: center; }
.sentinel .more-loading, .sentinel .end {
  font-family: var(--mono); font-size: 11px; color: var(--ink-3);
}
.sentinel .more-loading::after {
  content: ''; display: inline-block; width: 10px; height: 10px; margin-left: 8px;
  border: 2px solid var(--line-strong); border-top-color: var(--accent);
  border-radius: 999px; vertical-align: -1px;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.totop {
  position: fixed; right: 16px; z-index: 39;
  bottom: calc(38px + env(safe-area-inset-bottom, 0px));
  width: 38px; height: 38px; border-radius: 999px; border: 1px solid var(--line-strong);
  background: var(--surface); color: var(--ink-2); font-size: 16px; cursor: pointer;
  box-shadow: var(--shadow);
  opacity: 0; transform: translateY(12px); pointer-events: none;
  transition: opacity .18s, transform .18s;
}
.totop.is-shown { opacity: 1; transform: none; pointer-events: auto; }
.totop:hover { color: var(--ink); }

/*
 * The price column has a width, and nothing inside it may argue.
 *
 * "to KSh 1,250,000 elsewhere" is wider than the column it explains, and while
 * it sat in normal flow it set the column's minimum width and dragged the
 * whole table sideways on a phone. `width: 0` with `min-width: 100%` is the
 * table-cell ellipsis trick: the sub-line contributes nothing to the column's
 * intrinsic width, then fills whatever the column turned out to be.
 */
/* Wide enough for the longest headline the catalogue holds, and no wider: the
   pixels beyond that belong to the product name. */
td.price { width: 104px; }
td.price .price-n { display: block; }
td.price .sub, .pcard-price .sub {
  display: block; width: 0; min-width: 100%;
  font-family: var(--mono);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.price .sub.range { color: var(--ink-3); }
.pd-from { font-family: var(--mono); font-size: 11px; color: var(--ink-3); }

/* ---------- card view ---------- */

/*
 * Every card the same size, and the width used up rather than left trailing.
 *
 * The tracks are `1fr`, so a row of cards always spans the pane and every card
 * in it is the same width as every other: card width is a property of the
 * grid, not of the row, which is why a half-full last row still matches the
 * rows above it. That is the invariant, and it is why this is a grid and not a
 * flex row, where the last line stretches to different widths than the rest.
 *
 * `auto-fit` collapses tracks that hold nothing, which is the one case
 * `auto-fill` left ragged: a search matching three products at 1280px sat
 * three 174px cards on the left and six empty tracks to the right of them.
 * Collapsing lets those three grow, and the cap below is what stops growth
 * from turning three results into three billboards.
 *
 * The internal structure is fixed for a separate reason: a card whose height
 * depends on whether the title needed two lines makes the grid reflow every
 * time a page of results lands.
 */
.pcards {
  display: grid; align-items: stretch; justify-items: center;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 10px; padding: 4px 0 2px;
}
.pcard {
  /*
   * Fills its track, up to a card's natural reading width. In a full grid the
   * track is around 175px and the cap never binds, so this is the same layout
   * as before; it engages only where auto-fit has collapsed tracks. Centring
   * is what keeps the gaps even when it does: equal tracks holding equally
   * capped cards leave the same space between every pair.
   */
  width: 100%; max-width: 300px;
  display: flex; flex-direction: column; gap: 6px;
  padding: 8px 9px 9px;
  border: 1px solid var(--line); border-top: 3px solid var(--line);
  border-radius: 8px; background: var(--surface);
  transition: border-color .12s, box-shadow .12s, transform .12s;
}
.pcard:hover { box-shadow: var(--shadow); transform: translateY(-1px); }
.pcard.is-cursor { border-color: var(--accent); }
/* The evidence verdict as the card's top edge, which is the same job the left
   edge of the thumbnail cell does in the list. */
.pcard.ev-low { border-top-color: var(--ok); }
.pcard.ev-below { border-top-color: var(--accent); }
.pcard.ev-high { border-top-color: var(--warn); }
.pcard.ev-unproven { border-top-color: var(--line); }
/* A button, because it is the door to the detail dialog; the title beside it
   is the door to the product page. */
.pcard-img {
  aspect-ratio: 1 / 1; width: 100%; padding: 0; cursor: pointer;
  border-radius: 6px; overflow: hidden;
  background: var(--image-pad); border: 1px solid var(--line);
}
.pcard-img img { display: block; width: 100%; height: 100%; object-fit: contain; }
.pcard-img .noimg { display: block; width: 100%; height: 100%; background: var(--surface-3); }
.pcard-title {
  /* Exactly two lines, always, whether the name needs them or not. */
  height: 2.7em; line-height: 1.35; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  font-size: 12px; font-weight: 560; color: var(--ink);
}
.pcard-name { color: inherit; text-decoration: none; }
.pcard-name:hover { color: var(--accent); text-decoration: underline; }
.pcard-price {
  height: 32px; display: flex; flex-direction: column; justify-content: center;
  font-family: var(--mono); font-variant-numeric: tabular-nums;
}
.pcard-price .price-n { font-size: 13px; font-weight: 620; }
.pcard-chips {
  height: 18px; display: flex; align-items: center; gap: 4px;
  overflow: hidden; white-space: nowrap;
}
.pcard-chips .ev, .pcard-chips .badge, .pcard-chips .mm {
  font-family: var(--mono); font-size: 9.5px; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis;
}
.pcard-chips .mm {
  display: inline-block; padding: 0 5px; border-radius: 3px;
  background: var(--surface-3); color: var(--ink-2);
}

/* ---------- status bar ---------- */

/*
 * One thin line at the foot of the shell: what the query answered on the left,
 * which build answered it on the right. The last row of the frame rather than
 * a fixed overlay, so it is always visible without ever covering the last
 * product, and safe-area padded so it clears the home indicator on a phone.
 */
.statusbar {
  flex: none;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  height: calc(26px + env(safe-area-inset-bottom, 0px));
  padding: 0 12px env(safe-area-inset-bottom, 0px);
  background: var(--surface-2); border-top: 1px solid var(--line);
  color: var(--ink-3); font-family: var(--mono); font-size: 11px;
  font-variant-numeric: tabular-nums;
  overflow: hidden; white-space: nowrap;
}
.status-left { display: flex; align-items: center; gap: 12px; min-width: 0; overflow: hidden; }
.status-left .st-sum { color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; }
.statusbar .build { flex: none; max-width: 45%; overflow: hidden; text-overflow: ellipsis; opacity: .8; }

/* ---------- scrollbars ---------- */

/*
 * Thin, rounded, tinted from the palette, on every scrolling surface. The
 * standard properties cover Firefox and current Chromium; the -webkit rules
 * cover Safari and older Chromium, which ignore the standard ones. Chromium
 * 121 and later honour scrollbar-color and drop the -webkit styling, so the
 * two declarations never both apply.
 */
html { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
* { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--line-strong); border-radius: 999px;
  border: 2px solid transparent; background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--accent); background-clip: padding-box; }
::-webkit-scrollbar-corner { background: transparent; }
.multi-list::-webkit-scrollbar { width: 6px; }

/*
 * The filter group lists hide their scrollbar until the pointer is over them.
 * Only the thumb goes transparent, never the gutter, so nothing shifts when it
 * appears. On touch screens :hover never matches and the list scrolls with no
 * thumb at all, which is what native overlay scrollbars do there anyway. The
 * thumb-hover accent is restated last because it ties with the :hover reveal
 * on specificity and must win by source order.
 */
.multi-list { scrollbar-color: transparent transparent; }
.multi-list:hover { scrollbar-color: var(--line-strong) transparent; }
.multi-list::-webkit-scrollbar-thumb { background: transparent; background-clip: padding-box; }
.multi-list:hover::-webkit-scrollbar-thumb { background: var(--line-strong); background-clip: padding-box; }
.multi-list:hover::-webkit-scrollbar-thumb:hover { background: var(--accent); background-clip: padding-box; }

/* ---------- usage (ops) ---------- */

.usage-bars {
  display: flex; align-items: flex-end; gap: 2px; height: 44px; margin: 12px 0 4px;
}
.usage-bar {
  flex: 1; min-width: 3px; height: max(2px, var(--h, 0%)); background: var(--accent);
  border-radius: 2px 2px 0 0; opacity: .8;
}
.usage-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px 22px;
  margin-top: 12px;
}
.usage-list h3 { font-size: 11px; margin: 0 0 4px; color: var(--ink-3); text-transform: uppercase; letter-spacing: .04em; }
.usage-list ol { margin: 0; padding: 0 0 0 18px; font-size: 12px; }
.usage-list li { display: flex; gap: 8px; padding: 1px 0; }
.usage-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.usage-n { font-family: var(--mono); font-size: 11px; color: var(--ink-3); }

/*
 * Skeleton rows keep the layout stable while the query runs.
 *
 * The sheen used to be an animated `background-position` on the row itself,
 * which repaints the element on every frame for as long as the query takes,
 * on as many rows as are showing. It is a moving overlay now: the row is a
 * flat fill, the highlight is one absolutely positioned child, and the only
 * animated property is `transform`, which the compositor owns. Same picture,
 * no per-frame paint.
 *
 * The rows are removed when the results land, so the promotion an infinite
 * transform animation earns goes away with them and nothing carries a standing
 * `will-change`.
 */
.skeleton { padding: 6px 0; }
.skel-row {
  position: relative; overflow: hidden;
  height: 30px; border-radius: 6px; margin-bottom: 6px;
  background: var(--surface-2);
}
.skel-row::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 15%, var(--surface-3) 50%, transparent 85%);
  transform: translateX(-100%);
  animation: shimmer 1.1s linear infinite;
}
@keyframes shimmer { to { transform: translateX(100%); } }

/* ---------- product detail dialog ---------- */

dialog.pd {
  border: 0; padding: 0; border-radius: 10px;
  width: min(92vw, 860px);
  background: var(--surface); color: var(--ink);
  box-shadow: var(--shadow-lg);
}
dialog.pd::backdrop { background: var(--backdrop); backdrop-filter: blur(2px); }
dialog.pd[open] { animation: pd-in .18s ease; }
@keyframes pd-in { from { opacity: 0; transform: translateY(10px) scale(.98); } }

.pd-frame { display: flex; flex-direction: column; max-height: 86vh; }
.pd-head {
  display: flex; align-items: center; gap: 10px;
  padding: 11px 14px; border-bottom: 1px solid var(--line);
}
.pd-head-pad { flex: 1; }
/* Image, then title, then the one action: the standard preview order. The
   title is plain text; the labelled button below it is the outbound door. */
.pd-name { display: flex; flex-direction: column; gap: 3px; margin-top: 10px; }
.pd-title {
  margin: 0; color: var(--ink);
  font-weight: 640; font-size: 15px; line-height: 1.3;
}
/* The merchant's own title, under our corrected one. Our version is an opinion
   about their string, and the shopper matching this against the tab they
   already have open needs the string they will actually see there. */
.pd-raw { color: var(--ink-3); font-size: 11px; line-height: 1.35; }
.pd-close {
  border: 0; background: none; cursor: pointer;
  font-size: 20px; line-height: 1; color: var(--ink-3); padding: 0 2px;
}
.pd-close:hover { color: var(--ink); }

.pd-body {
  display: grid; grid-template-columns: 300px 1fr; gap: 16px 20px;
  padding: 14px 16px 18px; overflow-y: auto;
}
.pd-media { min-width: 0; }
.pd-imgbtn {
  display: block; width: 100%; padding: 0; border: 1px solid var(--line);
  border-radius: 8px; overflow: hidden; background: var(--image-pad); cursor: zoom-in;
}
.pd-imgbtn img {
  display: block; width: 100%; aspect-ratio: 1; object-fit: contain;
  transition: transform .22s ease;
}
.pd-imgbtn.is-zoomed { cursor: zoom-out; }
.pd-imgbtn.is-zoomed img { transform: scale(1.8); }
.pd-noimg {
  aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
  border: 1px dashed var(--line-strong); border-radius: 8px; color: var(--ink-3);
}

.pd-facts { min-width: 0; display: flex; flex-direction: column; gap: 12px; }
.pd-price { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.pd-price b {
  font-family: var(--mono); font-size: 20px; font-weight: 640;
  font-variant-numeric: tabular-nums; letter-spacing: -.02em;
}
.pd-price s { font-size: 12.5px; }
.pd-save {
  font-family: var(--mono); font-size: 11px; color: var(--ok);
  background: var(--ok-soft); border-radius: 999px; padding: 2px 9px;
}

.kv { display: grid; grid-template-columns: max-content 1fr; gap: 3px 12px; font-size: 12.5px; margin: 0; }
.kv dt { color: var(--ink-3); font-family: var(--mono); font-size: 10.5px; align-self: baseline; }
.kv dd { margin: 0; font-variant-numeric: tabular-nums; }

.pd-unit {
  font-family: var(--mono); font-size: 11px; color: var(--accent);
  background: var(--accent-soft); border-radius: 999px; padding: 2px 9px;
}

/* ---------- the verdict panel ---------- */

/*
 * Under the image, because a buyer looks at the picture first and the question
 * immediately after it is "is this real". Every score is followed by the
 * reasons behind it: a number nobody can account for is a number nobody
 * should act on.
 */
.pd-verdict {
  margin-top: 12px; padding: 11px 12px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 8px;
}
.verdict-scores { display: flex; gap: 8px; }
.vscore {
  flex: 1; text-align: center; padding: 6px 4px; border-radius: 6px;
  background: var(--surface); border: 1px solid var(--line);
}
.vscore b {
  display: block; font-family: var(--mono); font-size: 17px; font-weight: 640;
  font-variant-numeric: tabular-nums; letter-spacing: -.02em;
}
.vscore span { font-size: 9.5px; text-transform: uppercase; letter-spacing: .07em; color: var(--ink-3); }
.vscore.risk-low b { color: var(--warn); }
.vscore.risk-mid b { color: var(--warn); }
.vscore.risk-high b { color: var(--crit); }

.signals { margin: 10px 0 0; padding-left: 16px; font-size: 11.5px; color: var(--ink-2); line-height: 1.5; }
.signals li { margin-bottom: 3px; }
.signals li.sig-adj { color: var(--accent); }

/* ---------- other sellers ---------- */

.pd-offers { margin-top: 4px; }
.offers-list { display: flex; flex-wrap: wrap; gap: 5px; }
.offer {
  display: flex; flex-direction: column; gap: 1px;
  padding: 4px 9px; border: 1px solid var(--line); border-radius: 5px;
  background: var(--surface-2); text-decoration: none; color: var(--ink);
  font-family: var(--mono); font-size: 11.5px; font-variant-numeric: tabular-nums;
  transition: border-color .12s, transform .12s;
}
.offer:hover { border-color: var(--accent); transform: translateY(-1px); }
.offer .offer-meta { font-size: 9.5px; color: var(--ok); }
.offer.out { opacity: .5; }
.offer.out .offer-meta { color: var(--ink-3); }

.pd-cat { font-size: 12px; }
.pd-cat-line b { color: var(--ok); }
.cat-track {
  position: relative; height: 6px; margin: 7px 0 5px; border-radius: 999px;
  background: linear-gradient(90deg, var(--ok), var(--warn) 55%, var(--crit));
  opacity: .85;
}
.cat-marker {
  position: absolute; top: 50%; transform: translate(-50%, -50%);
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--surface); border: 3px solid var(--ink);
  box-shadow: var(--shadow-sm);
}
.pd-cat-meta { color: var(--ink-3); font-family: var(--mono); font-size: 10.5px; }

.pd-history { grid-column: 1 / -1; border-top: 1px solid var(--line); padding-top: 12px; }
.pd-history h3, .vars h3, .pd-history .meta { font-size: 11px; }
.pd-history h3, .vars h3 {
  margin: 0 0 6px; font-family: var(--mono); font-size: 10px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-3); font-weight: 600;
}
.spark { width: 100%; max-width: 480px; height: 80px; display: block; }
.vars { display: flex; flex-direction: column; gap: 3px; font-size: 12px; }
.vars div { display: flex; justify-content: space-between; gap: 10px; font-family: var(--mono); }
.vars .out { color: var(--ink-3); text-decoration: line-through; }
.buy {
  display: inline-block; margin-top: 12px; padding: 8px 18px;
  background: var(--accent); color: var(--surface); border-radius: 5px;
  text-decoration: none; font-weight: 600; font-size: 13px;
  transition: transform .12s, box-shadow .12s;
}
.buy:hover { transform: translateY(-1px); box-shadow: var(--shadow); }
/* The dialog's primary action: full width of the media column, under the
   title, where a shop's buy button lives. The verdict panel follows it. */
.pd-open { display: block; text-align: center; margin: 10px 0 14px; }

/* ---------- the copy toast ---------- */

/*
 * The floating Filters button that used to live here is gone. It sat over the
 * last row of the table at every scroll position, and the same door now stands
 * in the results bar beside the other controls, where it can carry a count and
 * cannot cover anything.
 */
.toast {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(46px + env(safe-area-inset-bottom, 0px)); z-index: 48;
  padding: 7px 14px; border-radius: 999px;
  background: var(--ink); color: var(--ground);
  font-size: 12px; font-weight: 560;
  box-shadow: var(--shadow-md);
  animation: settle .16s ease;
}
/* The clipboard fallback's scratch field, which must never be seen or read. */
.copypad { position: fixed; top: -1000px; left: -1000px; opacity: 0; }

/* ---------- ops ---------- */

.ops { padding: 16px; display: flex; flex-direction: column; gap: 16px; max-width: 1200px; }
.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 5px; padding: 16px;
}
.card > h2 {
  margin: 0 0 12px; font-family: var(--mono); font-size: 11px;
  letter-spacing: .12em; text-transform: uppercase; color: var(--ink-3); font-weight: 600;
}
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 5px; overflow: hidden; }
.tile { background: var(--surface); padding: 12px 14px; }
.tile .v {
  display: block; font-family: var(--mono); font-size: 20px; font-weight: 600;
  font-variant-numeric: tabular-nums; letter-spacing: -.02em;
}
.tile .k { font-size: 12px; color: var(--ink-2); }
.tile.good .v { color: var(--ok); }
.tile.flag .v { color: var(--crit); }

.controls { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; }
.controls .field { min-width: 150px; }
.btn {
  border: 1px solid var(--accent); background: var(--accent); color: var(--surface);
  padding: 7px 18px; border-radius: 4px; cursor: pointer; font-weight: 600;
}
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn.ghost { background: transparent; color: var(--accent); }
.btn.stop { border-color: var(--crit); background: var(--crit); }

.progress { margin-top: 14px; }
.bar { height: 8px; background: var(--surface-3); border-radius: 999px; overflow: hidden; }
.bar > span { display: block; height: 100%; background: var(--accent); transition: width .3s; }
.ticker {
  margin-top: 8px; font-family: var(--mono); font-size: 12px;
  color: var(--ink-2); font-variant-numeric: tabular-nums;
  display: flex; flex-wrap: wrap; gap: 14px;
}
.ticker .skip { color: var(--ok); }
.ticker .err { color: var(--crit); }
.streamstate { font-family: var(--mono); font-size: 11px; color: var(--ink-3); }
.streamstate.off { color: var(--warn); }
.log {
  margin-top: 10px; max-height: 160px; overflow-y: auto;
  font-family: var(--mono); font-size: 11.5px; color: var(--ink-2);
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 4px; padding: 8px; white-space: pre-wrap;
}

table.plain { width: 100%; border-collapse: collapse; font-size: 13px; }
table.plain th {
  text-align: left; font-family: var(--mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-3); font-weight: 600;
  padding: 0 10px 7px 0; border-bottom: 1px solid var(--line-strong); white-space: nowrap;
}
table.plain td { padding: 6px 10px 6px 0; border-bottom: 1px solid var(--line); }
table.plain td.r { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
.capped { color: var(--crit); font-family: var(--mono); font-size: 11px; }

/* Explanatory text inside a card: present, but never competing with the data. */
.muted { margin: 0 0 4px; color: var(--ink-3); font-size: 12px; max-width: 62ch; }
/* A filesystem path is an identifier to be copied exactly, so it gets the mono
 * face and is allowed to break rather than widen the card. */
.mono-path { font-family: var(--mono); font-size: 11px; word-break: break-all; }

/* ---------- states ---------- */

.empty, .loading { padding: 48px 16px; text-align: center; color: var(--ink-3); }
.empty b { display: block; color: var(--ink); margin-bottom: 6px; font-size: 15px; }
/* The offer a failed search makes, on its own line under the explanation. */
.empty .ctl { display: block; margin: 12px auto 0; }

.banner {
  position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%);
  z-index: 50; max-width: 640px;
  background: var(--crit-soft); color: var(--crit);
  border: 1px solid var(--crit); border-radius: 5px;
  padding: 10px 16px; font-size: 13px;
  display: flex; align-items: center; gap: 14px;
  box-shadow: var(--shadow-md);
}
.banner button { background: none; border: 0; color: inherit; cursor: pointer; font-size: 16px; }

/* ---------- responsive: three steps down ---------- */

/* Wide desktop keeps everything. Below 1200px the category column goes. */
@media (max-width: 1200px) {
  .hide-lg { display: none; }
  /* And the rail gives back thirty pixels, because between 900 and 1200 the
   * rail is still docked and the table still has nine columns to place. */
  :root { --rail: 206px; }
}

/*
 * Below 900px the rail becomes a slide-in drawer behind a floating Filters
 * button, and the secondary numeric columns go. The drawer is the same DOM
 * node as the desktop rail, so filter state never moves.
 */
@media (max-width: 900px) {
  .hide-md { display: none; }
  .filtersbtn { display: inline-flex; }
  .rail {
    position: fixed; left: 0; top: 0; bottom: 0; z-index: 60;
    height: 100dvh; width: min(320px, 86vw);
    border-right: 1px solid var(--line-strong);
    box-shadow: var(--shadow-drawer);
    transform: translateX(-103%);
    transition: transform .22s ease;
  }
  .drawer-open .rail { transform: none; }
  .rail-backdrop {
    display: block; position: fixed; inset: 0; z-index: 55;
    background: var(--scrim);
    opacity: 0; pointer-events: none; transition: opacity .2s;
  }
  .drawer-open .rail-backdrop { opacity: 1; pointer-events: auto; }
  .railclose { display: block; }
  .resbar { padding: 10px 10px 8px; }
  .listing { padding: 6px 10px 24px; }
}

/* Phones: two-line rows carry brand and saving as sublines, evidence lives in
 * the thumb rail's coloured edge, and the dialog becomes a full sheet. */
@media (max-width: 640px) {
  .hide-sm { display: none; }
  .m-only { display: block; }
  .topbar { gap: 4px 8px; padding: 4px 8px 6px; }

  /*
   * The bar becomes two rows: who this is and the menu button on the first,
   * the search box across the whole of the second. A bar carrying a brand, a
   * search field and a button at 360px is a bar that overflows.
   *
   * The menu itself needs no phone layout any more. It is the same dropdown at
   * every width, so the only thing that changes here is where the search box
   * sits and how tight the padding is.
   */
  .searchslot { order: 10; flex-basis: 100%; max-width: none; margin-right: 0; }
  .topmenu { right: 6px; }
  .menubtn { margin-left: auto; }

  .resbar { padding: 8px 8px; }
  .listing { padding: 4px 8px 24px; }
  /* The table must FIT the phone, and if a future column defeats that, it
   * scrolls inside its own container rather than dragging the page sideways.
   * The scroll container changes what sticky is relative to, so a sticky
   * header offsets itself down INTO the table; static is correct here. */
  .tablewrap { overflow-x: auto; }
  table.grid th { position: static; }
  table.grid td { vertical-align: top; padding-top: 5px; padding-bottom: 5px; }
  table.grid th.r, table.grid td.r { padding-right: 6px; }
  table.grid th { padding-right: 6px; }
  td.name a {
    white-space: normal;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden;
  }
  /* The thumbnail keeps its size on a phone. It is the only picture the row
     has, and 40px of a product photo is a coloured smudge. */
  .thumbbtn:hover img { transform: none; box-shadow: none; }
  td.thumb { width: 64px; padding-right: 6px; }

  /*
   * The phone row is three columns and two lines: thumb, name, price on
   * the first line; the facts the dropped columns carried on the second,
   * as short chips that wrap inside the name column. The vs-90d column is
   * folded into that line too, so the table is thumb | name | price and
   * never wider than the screen. Sorting still lives in the headers and
   * the sort box.
   */
  /* Fixed layout takes its column widths from the header row, so the two
     narrow columns are sized there and the name column gets the rest. */
  table.grid { table-layout: fixed; width: 100%; }
  table.grid th.r:not(.price-h), table.grid td.r:not(.price):not(.name) { display: none; }
  table.grid th.thumb { width: 64px; }
  table.grid th.price-h, td.price { width: 84px; }
  td.price .sub { font-size: 9.5px; }
  /* The name cell becomes one wrapping line under the title: badges and
     chips flow together, so a row is the title plus one or two short lines. */
  td.name { display: flex; flex-wrap: wrap; align-content: flex-start; gap: 3px 5px; }
  td.name > .ptitle { flex-basis: 100%; min-width: 0; }
  td.name .sub { display: none; }
  td.name .badges, .m-meta { display: contents; }
  td.name .badge { margin: 0; }
  .mm-brand { color: var(--ink); font-weight: 600; }
  .mm {
    display: inline-block; font-family: var(--mono); font-size: 9.5px; line-height: 1.5;
    padding: 0 5px; border-radius: 3px; background: var(--surface-3); color: var(--ink-2);
    white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  }
  .mm-pos { background: var(--ok-soft); color: var(--ok); }
  .mm-claim { color: var(--ink-3); }
  .mm-inflated { text-decoration: line-through; text-decoration-color: var(--crit); }
  .mm-cat { background: transparent; border: 1px solid var(--line); color: var(--ink-3); }
  .mm.ev { font-size: 9.5px; }

  /* A strip of backdrop stays visible on every side, so the tap-outside
     dismissal that works on desktop still has somewhere to land on a phone.
     The gutter is paid for out of the dialog's own padding rather than out of
     the content, which is why the padding below tightens at the same time. */
  dialog.pd {
    width: calc(100vw - 24px); max-width: calc(100vw - 24px);
    height: calc(100dvh - 24px); max-height: calc(100dvh - 24px);
    margin: 12px auto; border-radius: 10px;
  }
  dialog.pd[open] { animation: pd-up .22s ease; }
  .pd-frame { max-height: calc(100dvh - 24px); height: 100%; }
  .pd-head { padding: 8px 9px; }
  .pd-body { grid-template-columns: 1fr; padding: 10px 9px 12px; gap: 12px; }
  .pd-media { max-width: 320px; margin: 0 auto; }
}
@keyframes pd-up { from { opacity: 0; transform: translateY(28px); } }

/* ---------------------------------------------------------------------------
 * Jobs: telemetry for the background work.
 *
 * Cards rather than a table, because each job carries six facts plus free text,
 * and a table wide enough for that scrolls sideways on a phone. Colour follows
 * the same rule as the rest of the panel: it means a state is true, never
 * decoration. A failing job gets a red rail; a running one gets an accent rail.
 * ------------------------------------------------------------------------- */

.jobs { padding: 16px; display: flex; flex-direction: column; gap: 14px; }

.jobs-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.jobs-head h2 { margin: 0; font-size: 16px; letter-spacing: -0.01em; }
.jobs-sub { color: var(--ink-3); font-size: 12.5px; font-family: var(--mono); }

.jobs-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
  gap: 12px;
}

.jobs-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--line-strong);
  border-radius: 4px;
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.jobs-card.is-running { border-left-color: var(--accent); }
.jobs-card.is-failing { border-left-color: var(--crit); }

.jobs-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.jobs-card-head h3 { margin: 0; font-size: 14px; font-family: var(--mono); font-weight: 600; }

.jobs-facts {
  margin: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px 10px;
}
.jobs-fact dt {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-3);
}
.jobs-fact dd {
  margin: 1px 0 0;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  color: var(--ink);
}

.jobs-msg, .jobs-err, .jobs-warn {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.45;
  /* Job output is a server string of unknown length; wrapping beats clipping. */
  overflow-wrap: anywhere;
}
.jobs-msg  { color: var(--ink-2); }
.jobs-err  { color: var(--crit); font-family: var(--mono); font-size: 12px; }
.jobs-warn { color: var(--warn); }

.jobs-card-foot { display: flex; justify-content: flex-end; }
.jobs-run {
  font: inherit;
  font-size: 12.5px;
  padding: 4px 12px;
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  background: var(--surface-2);
  color: var(--ink);
  cursor: pointer;
}
.jobs-run:hover:not(:disabled) { background: var(--surface-3); }
.jobs-run:disabled { opacity: 0.55; cursor: default; }

.pill {
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 2px 8px;
  border-radius: 999px;
  white-space: nowrap;
}
.pill-ok      { background: var(--ok-soft);     color: var(--ok); }
.pill-bad     { background: var(--crit-soft);   color: var(--crit); }
.pill-warn    { background: var(--warn-soft);   color: var(--warn); }
.pill-running { background: var(--accent-soft); color: var(--accent); }
.pill-idle    { background: var(--surface-3);   color: var(--ink-3); }

/* The key prompt and the empty state share a centred, narrow column: both are
 * one short message, and full width would make them look like a failure. */
.jobs-key, .jobs-empty {
  max-width: 46ch;
  margin: 32px auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 22px 24px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.jobs-key h2, .jobs-empty h2 { margin: 0; font-size: 15px; }
.jobs-key p, .jobs-empty p { margin: 0; color: var(--ink-2); font-size: 13px; line-height: 1.5; }

.jobs-key-row { display: flex; gap: 8px; margin-top: 4px; }
.jobs-key-input {
  flex: 1;
  font: inherit;
  font-family: var(--mono);
  font-size: 13px;
  padding: 6px 10px;
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  background: var(--surface-2);
  color: var(--ink);
  min-width: 0;
}
.jobs-key button {
  font: inherit;
  font-size: 13px;
  padding: 6px 16px;
  border: 1px solid var(--accent);
  border-radius: 3px;
  background: var(--accent);
  color: var(--surface);
  cursor: pointer;
}

@media (max-width: 520px) {
  .jobs-facts { grid-template-columns: repeat(2, 1fr); }
}

/* ---------------------------------------------------------------------------
 * Alerts: turning notifications on. Public, and used from a phone, so the
 * targets are larger than the rest of the panel and the column is narrow.
 * ------------------------------------------------------------------------- */

.alerts { padding: 16px; }

.alerts-card {
  max-width: 52ch;
  margin: 24px auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 22px 24px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.alerts-card h2 { margin: 0; font-size: 17px; letter-spacing: -0.01em; }
.alerts-note { margin: 0; color: var(--ink-2); font-size: 13.5px; line-height: 1.55; }
.alerts-meta { margin: 0; color: var(--ink-3); font-size: 12.5px; font-family: var(--mono); }
.alerts-warn {
  margin: 0; color: var(--warn); font-size: 13px; line-height: 1.5;
  background: var(--warn-soft); border-radius: 3px; padding: 10px 12px;
}

.alerts-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--ink-3); margin-top: 6px;
}

.alerts-chips { display: flex; gap: 8px; flex-wrap: wrap; }
.alerts-chip {
  font: inherit;
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  padding: 8px 14px; min-width: 68px;
  border: 1px solid var(--line-strong); border-radius: 4px;
  background: var(--surface-2); color: var(--ink); cursor: pointer;
}
.alerts-chip b { font-family: var(--mono); font-size: 14px; font-weight: 600; }
.alerts-chip span { font-size: 10.5px; color: var(--ink-3); }
.alerts-chip.is-on {
  border-color: var(--accent); background: var(--accent-soft); color: var(--accent);
}
.alerts-chip.is-on span { color: var(--accent); }

.alerts-input {
  font: inherit; font-family: var(--mono); font-size: 14px;
  padding: 9px 12px;
  border: 1px solid var(--line-strong); border-radius: 4px;
  background: var(--surface-2); color: var(--ink);
  width: 100%;
}

.alerts-actions { margin-top: 6px; }
.alerts-btn {
  font: inherit; font-size: 14px; font-weight: 500;
  /* Full width: this is the one action on the page, and it is tapped. */
  width: 100%; padding: 11px 16px;
  border: 1px solid var(--accent); border-radius: 4px;
  background: var(--accent); color: var(--surface); cursor: pointer;
}
.alerts-btn:disabled { opacity: 0.6; cursor: default; }
.alerts-btn-off {
  background: var(--surface-2); color: var(--crit); border-color: var(--line-strong);
}

/* -- the shell dialogs ---------------------------------------------------- */

/*
 * Alerts and About, as one shape.
 *
 * Both were pages: alerts a tab that threw away the shop underneath it to show
 * four buttons, the disclosure a footer line nobody scrolls a table of a
 * quarter of a million rows to reach. Each is a card of text, and a card of
 * text over the shop is a dialog. They stay centred on a phone rather than
 * becoming full sheets, because the content is short and a full sheet for six
 * lines reads as a page the visitor has been sent to.
 */
dialog.sheet {
  border: 0; padding: 0; border-radius: 10px;
  width: min(92vw, 460px);
  background: var(--surface); color: var(--ink);
  box-shadow: var(--shadow-lg);
}
dialog.sheet::backdrop { background: var(--backdrop); backdrop-filter: blur(2px); }
dialog.sheet[open] { animation: pd-in .18s ease; }
/* The frame holds still and the body scrolls, so the close button is reachable
   however long the card gets. */
.sheet-frame { position: relative; }
.sheet-body { max-height: 86dvh; overflow-y: auto; }
.sheet-close {
  position: absolute; top: 6px; right: 8px; z-index: 1;
  border: 0; background: none; cursor: pointer;
  font-size: 20px; line-height: 1; color: var(--ink-3); padding: 2px 4px;
}
.sheet-close:hover { color: var(--ink); }
.sheet-body { padding: 18px 22px 22px; }
.sheet-body h2 { margin: 0 0 12px; font-size: 17px; letter-spacing: -0.01em; }
.sheet-body p { margin: 0 0 10px; color: var(--ink-2); font-size: 13.5px; line-height: 1.55; }
.sheet-stats:empty { display: none; }
/* The full stamp, in the face an identifier belongs in, because this is the
   line an operator is asked to read back. */
.sheet-build {
  font-family: var(--mono); font-size: 10.5px; color: var(--ink-3);
  word-break: break-all;
}
/* The alerts card is the dialog's whole body, so it drops the framing it
   needed as a page of its own. */
.sheet-alerts .alerts { padding: 0; }
.sheet-alerts .alerts-card {
  margin: 0; max-width: none;
  border: 0; padding: 0; background: none;
}

/*
 * The privacy disclosure, folded into About.
 *
 * Fine print that a person can actually finish, so it is set as body copy and
 * not as a control panel: the same size as the paragraphs above it, a looser
 * line height than anything in the table, and a measure that stops well short
 * of the dialog's edge. It is deliberately not a warning banner. A disclosure
 * styled to alarm is one people close, and this one has something to say.
 *
 * These rules come after `.sheet-body p`, which is the same specificity, and
 * source order is what decides them. Moving this block above that one would
 * silently give the disclosure the dialog's paragraph spacing back.
 */
details.privacy {
  margin: 14px 0 2px;
  border-top: 1px solid var(--line);
  padding-top: 10px;
}

/*
 * Open and closed both look decided: closed it is one quiet line on the
 * dialog's own surface, open it is a panel with a fill and the rule above it
 * gone, so the fold reads as a place rather than as text that appeared.
 */
details.privacy[open] {
  border-top-color: transparent;
  background: var(--surface-2);
  border-radius: 8px;
  padding: 10px 12px 12px;
}

details.privacy > summary {
  display: flex; align-items: center; gap: 8px;
  cursor: pointer;
  margin: -2px -4px; padding: 4px; border-radius: 5px;
  font-size: 12.5px; font-weight: 560; color: var(--ink-2);
  transition: color .12s;
}
details.privacy > summary:hover { color: var(--ink); }
details.privacy[open] > summary { color: var(--ink); margin-bottom: 6px; }
details.privacy > summary:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px;
}

/*
 * The disclosure marker, replaced rather than restyled.
 *
 * A user-agent marker is drawn in the UA's own colour, not in `currentColor`,
 * so it survives every token in this file and is the one thing that looks
 * wrong in a dark theme: a black arrow on a near-black card. Both spellings
 * are removed, WebKit's pseudo-element and the standard `::marker`, because
 * either one alone leaves it showing in half the browsers.
 *
 * What replaces it is a caret made of borders, which takes `--ink-3` like the
 * rest of the fine print, and turns on `transform` alone.
 */
details.privacy > summary::-webkit-details-marker { display: none; }
details.privacy > summary::marker { content: ''; }
details.privacy > summary::before {
  content: ''; flex: none;
  width: 0; height: 0;
  border-left: 5px solid var(--ink-3);
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform .15s ease;
}
details.privacy[open] > summary::before { transform: rotate(90deg); }
details.privacy > summary:hover::before { border-left-color: var(--ink-2); }

/* Prose, at prose spacing. The last paragraph carries the opt-out, so it keeps
   its bottom margin rather than being collapsed against the panel edge. */
details.privacy p {
  margin: 0 0 9px;
  font-size: 12.5px; line-height: 1.6; color: var(--ink-2);
  max-width: 58ch;
}
details.privacy p:last-child { margin-bottom: 0; }

ul.privacy-list {
  margin: 0 0 10px; padding-left: 20px;
  font-size: 12.5px; line-height: 1.6; color: var(--ink-2);
  max-width: 58ch;
}
ul.privacy-list li { margin-bottom: 5px; }
ul.privacy-list li:last-child { margin-bottom: 0; }
/* The bullet is a marker too, and takes the UA colour for the same reason the
   caret did. */
ul.privacy-list li::marker { color: var(--ink-3); }

/*
 * The opt-out is a link and is underlined like one, in the quiet weight the
 * rest of the fold uses. It changes what the site records about this browser,
 * so it is never allowed to become a colour difference alone.
 */
a.privacy-out {
  color: var(--link);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: var(--line-strong);
  transition: text-decoration-color .12s;
}
a.privacy-out:hover { text-decoration-color: var(--link); }
a.privacy-out:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px;
}
