/*
 * 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;
  --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;
}

* { box-sizing: border-box; }

/*
 * 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 below is opt-out in one place. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; 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 one box that is not a box on a desktop.
 *
 * `display: contents` makes the three controls direct flex children of the
 * bar, so the wide layout reads left to right in the order below. At 640px the
 * box becomes real again and turns into the dropdown, which is why there is
 * only ever one copy of each control and therefore only one place for its
 * state to live.
 */
.topmenu { display: contents; }
/* The slot takes the middle and gives the rest back, which is what holds the
   three controls and the menu button against the right edge. */
.searchslot { order: 1; flex: 1; min-width: 0; max-width: 560px; margin-right: auto; }
.topbar #alertsbtn { order: 2; }
.topbar #aboutbtn { order: 3; }
.topbar .themes { order: 4; }
.menubtn { display: none; order: 5; }
/* 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 control: three states, one tap each, no menu.
 *
 * A menu would hide "System" behind an interaction, and System is the state
 * most visitors should be in. Segmented, so the three read as one setting, but
 * borderless like everything else in the bar: the chosen one is named in the
 * accent and the other two are quiet.
 */
.themes { display: flex; gap: 2px; }
.themes button {
  appearance: none; border: 0; background: transparent; color: var(--ink-3);
  padding: 5px 8px; border-radius: 5px; cursor: pointer;
  font: inherit; font-size: 11.5px; font-weight: 560;
  transition: color .12s, background .12s;
}
.themes button:hover { background: var(--surface-3); color: var(--ink); }
.themes button.is-active { color: var(--accent); }
.themes button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/*
 * The phone's one bar 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.
 */
.menubtn {
  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. */
.skeleton { padding: 6px 0; }
.skel-row {
  height: 30px; border-radius: 6px; margin-bottom: 6px;
  background: linear-gradient(100deg, var(--surface-2) 40%, var(--surface-3) 50%, var(--surface-2) 60%);
  background-size: 200% 100%;
  animation: shimmer 1.1s linear infinite;
}
@keyframes shimmer { to { background-position: -200% 0; } }

/* ---------- 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 one button on the first, the
   * search box across the whole of the second. Everything else moves into the
   * dropdown that button opens, because a bar carrying a brand, a search field
   * and three controls at 360px is a bar that overflows.
   *
   * The panel is the same DOM as the wide layout's inline controls, so there
   * is no second copy of any switch and nothing to keep in step. The search
   * box is the same input too: it is one node that changes place, not two that
   * would have to be kept in step.
   */
  .searchslot { order: 10; flex-basis: 100%; max-width: none; margin-right: 0; }
  .topmenu {
    display: none;
    position: absolute; top: 100%; right: 6px; z-index: 30;
    flex-direction: column; align-items: stretch; gap: 4px;
    min-width: 180px; padding: 8px;
    background: var(--surface);
    border: 1px solid var(--line-strong); border-radius: 8px;
    box-shadow: var(--shadow-md);
  }
  .topbar.menu-open .topmenu { display: flex; }
  .topmenu .topbtn { text-align: left; padding: 8px 10px; }
  .topmenu .themes { justify-content: stretch; border-top: 1px solid var(--line); padding-top: 6px; }
  .topmenu .themes button { flex: 1; padding: 8px; }
  .menubtn { display: flex; 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;
}
