/* AGRES MPL Platform — design system.
 *
 * ORIGIN NOTE (this changed, read before comparing against the mockup):
 * this file began as a verbatim lift from agres_bundle/docs/webapp_mockup.html
 * and no longer is one. The mockup remains the record of the ORIGINAL layout
 * and is still where the class names come from, but it is no longer the
 * visual target — the palette, type scale, spacing scale, focus states and
 * iconography below deliberately diverge from it. If the two disagree, this
 * file wins. See README.md → "The visual language".
 *
 * WHAT THIS APP IS, because it decides everything below: an internal
 * operations console that three people use every working day to clear a
 * queue. It is not a marketing page and not a general-purpose dashboard.
 * Density, scannability and never-lying-about-state matter; delight is a
 * distant fourth. Every rule here should be defensible as "this makes the
 * queue faster or safer to work", not "this looks nice".
 *
 * *** THE AGRES LOOK (owner, 26 Sep 2026: "it feels so dull and
 * uninspiring", with agreskomputer.com as the brand to follow). *** Delight
 * moved up the list, on one condition that keeps the paragraph above true:
 * every effect CARRIES something. The brand band marks where the brand
 * speaks, never data; a figure counts up to the value it already shows; a
 * bar grows to its own length; the ring IS the completion figure; a
 * celebration fires only when every tracked product is done (never on a
 * rounded 100%). All motion stops under prefers-reduced-motion and in print.
 * See the "THE AGRES LOOK" sections below, and AGENTS.md's visual-language
 * notes for the traps this cost to learn.
 *
 * The organising idea is the ACTION WATERFALL. Every product/store pair
 * falls through BLACKLIST -> NOT REQUIRED -> INPUT PRODUCT -> INPUT PRODUCT
 * (SECONDARY) -> ACTIVATE LISTING -> UPDATE STOCK -> UPDATE PRICE -> OK and
 * lands in exactly one bucket. The colour language below is that cascade,
 * and it is information rather than decoration: the same hue means the same
 * action on the sidebar dot, the sub-tab, the pill in the table and the
 * progress bar. Changing one without the others breaks the language.
 *
 * ONE DELIBERATE BORROWING, recorded here so it is a decision and not a
 * drift. The listing-rules tier control (Primary / Secondary / Skip) reuses
 * three of these hues — OK's green, UPDATE PRICE's amber, UPDATE STOCK's
 * red — for a different concept: not "what to do about this product" but
 * "how much does this slice count". The owner asked for exactly that, and
 * the reasoning is that the two languages are never read as one sentence:
 * an action pill answers a question about one product on a worklist, and a
 * tier answers a question about a whole category on a settings screen.
 *
 * The seam to watch is CategoryRuleEditor's drawer, which shows both at
 * once — a green "Primary" heading above a blue INPUT PRODUCT pill. That
 * is the one place the borrowing is visible, and it survives because the
 * pill is quoting what the worklist WILL say, not labelling the choice.
 * If a third screen ever puts them side by side as equals, revisit this.
 */

/* ============================================================ TOKENS */
:root{
  /* ---- ink ----------------------------------------------------------
   * Three steps, and ALL THREE must stay legible, because all three carry
   * real data rather than chrome. --ink3 in particular is not "decorative
   * grey": store codes, notes, every table header and the entire "Correct?"
   * column when null are set in it. It used to be #8592a6, which measures
   * 3.15:1 on --bg and fails WCAG AA outright; table headers measured
   * ~2.8:1. The values below are the darkest the old design language can
   * take while still reading as a hierarchy: --ink3 is now 4.6:1 on --bg
   * and 5.0:1 on --panel. Do not lighten these back for looks.
   *
   * *** BUT NOT ON --line2, WHICH IS WHERE TABLE HEADERS SIT (24 Sep 2026).
   * *** --ink3 on the header ground is 4.39:1 — under AA on every table —
   * so `thead th` uses --ink2 now, and PaletteContrastTest measures the
   * heading pair in every theme. "Every table header" above describes how
   * it was. */
  --ink:#171f2e;        /* primary text — near-black slate           15.4:1 */
  --ink2:#47536b;       /* secondary text                             7.7:1 */
  --ink3:#64708a;       /* muted, but still real data                 4.6:1 */

  --line:#dfe5ee;       /* hairlines */
  --line2:#eef1f6;      /* faint fills — table head, inset rows */
  --bg:#f5f7fa;         /* app background — cool paper */
  --panel:#ffffff;      /* surfaces */

  /* ---- the surfaces a THEME has to be able to INVERT -----------------
   * These were literal hexes in 32 component rules until a dark theme
   * needed them. On a light theme a control well is WHITE and a hover is
   * a faint blue wash; on a dark one the well SINKS below the panel and
   * the hover LIFTS above it. That inversion is the whole reason these
   * are separate tokens rather than folded into --panel and --line2 —
   * one shared "off-white" cannot go two directions at once.
   *
   * Every value here is byte-identical to the literal it replaced, so the
   * default and colourblind themes render exactly as before — that
   * invariant is what makes a 32-rule refactor reviewable without
   * re-testing both light themes, so it is worth a token to keep rather
   * than a near-enough fold. TWO deliberate exceptions, both invisible and
   * both recorded here rather than left to be rediscovered:
   *   - .cat-brow's bottom hairline was #eef3fb and is now --line2
   *     (#eef1f6), a 1.5% difference on a 1px line;
   *   - .cat-brow:hover was #f2f7ff and is now --row-hover. That rule is
   *     DEAD either way — .cat-crow:hover,.cat-brow:hover later in the file
   *     has equal specificity and wins — so it renders nothing today. */
  --control-bg:#ffffff;   /* inputs, selects, ghost buttons — the well */
  --inset:#fbfdff;        /* .addbar, .ruleform — a toolbar inset in a panel */
  --disabled-bg:#f2f4f8;  /* a field that cannot be typed into */
  --row-hover:#f9fbff;    /* a table row under the pointer */
  --hover-strong:#e7ebf2; /* the same idea one step firmer — sortable heads */
  --sec-hover:#e2e7f0;    /* firmer still — a section chip, which is bigger
                           * than a row and needs a heavier press to read */
  --lift:#ffffff;         /* a hover that moves TOWARDS the reader. Not
                           * --panel: on a dark theme this must go lighter
                           * while --panel stays where it is. */

  /* The accent is AGRES rose (owner, 26 Sep 2026: follow agreskomputer.com).
   * It was blue (#2f6df0, then a deeper blue) until then. What has not
   * changed is the reason it is neither orange nor green: this app serves
   * BOTH Shopee (orange) and Tokopedia (green), and either of those hues as
   * the interface accent would silently imply a platform on every screen,
   * including the ones showing the other platform's stores. Rose is the
   * brand's own colour and belongs to neither marketplace. */
  --accent:#be185d;        /* AGRES rose — 6.0:1 under white */
  --accent-hover:#9d174d;
  --accent-soft:#fdf0f6;
  --accent-line:#f6c6db;

  /* ---- the AGRES brand (design prototype, 26 Sep 2026) -------------
   * From agreskomputer.com: the red of the swirl mark and the rose ->
   * purple -> cyan band its navigation sits on. The gradient is a
   * SIGNATURE, used where the brand speaks (the rail's active row, the
   * topbar's edge, the completion ring, primary buttons) and never as
   * the ground for data, so it cannot be mistaken for an action hue. */
  --brand-red:#ed1e27;
  --brand-a:#e11d48; --brand-b:#9333ea; --brand-c:#22d3ee;
  --brand-grad:linear-gradient(90deg,var(--brand-a),var(--brand-b) 60%,var(--brand-c));
  /* The fill a white label sits on: darker stops of the same band so
   * both ends clear 4.5:1 (#c8123f 5.6:1, #7e22ce 6.9:1). */
  --brand-fill:linear-gradient(100deg,#c8123f,#7e22ce);
  /* The soft shadow under a brand-filled control. Its own token so the
   * Colourblind theme can take the hue out and Dark can make it orange. */
  --brand-glow:rgba(200,18,63,.26);
  /* The light that sweeps a podium or mover card: across the whole card and
   * its text, so a theme with light text on a dark card must dim it. */
  --sheen:rgba(255,255,255,.55);
  /* People (initials badges): brand-side hues, none of them an action hue,
   * each taking white initials at 5.4:1 or better. */
  --av-1:#be185d; --av-2:#7e22ce; --av-3:#0e7490; --av-4:#1c274c; --av-5:#475569;
  /* The podium's medals; the numeral on each measures 5.3:1 or better. */
  --medal-1:linear-gradient(145deg,#fbe38a,#d9a520);
  --medal-2:linear-gradient(145deg,#f1f3f6,#aab2bf);
  --medal-3:linear-gradient(145deg,#f7cfa6,#c98a52);
  --medal-1-tx:#3a2a05; --medal-2-tx:#262c36; --medal-3-tx:#3a1d06;
  /* The podium cards: each medal's metal as a wash, an edge and a glow. */
  --podium-1-bg:linear-gradient(135deg,#fdeeb4 0%,#fff8df 35%,#ffffff 70%); --podium-1-line:#e6c461; --podium-1-glow:rgba(217,165,32,.28);
  --podium-2-bg:linear-gradient(135deg,#d9dfe8 0%,#eef1f5 35%,#ffffff 70%); --podium-2-line:#b3bccb; --podium-2-glow:rgba(90,102,125,.24);
  --podium-3-bg:linear-gradient(135deg,#f8dcc3 0%,#fcefe3 35%,#ffffff 70%); --podium-3-line:#dfb088; --podium-3-glow:rgba(201,138,82,.24);
  --podium-1-tx:#6b4e00; --podium-1-chip:#fbe7a6;
  /* The swirl's two reds, from the logo file itself. */
  --brand-mark:#e22a2a; --brand-mark-deep:#c72328;

  /* Text and chrome that sit ON an accent fill: every primary button, the
   * active nav row, the on sub-tab, the division pills. This was a literal
   * #fff in nine rules, which is correct while the accent is a deep blue
   * and fails outright the moment a theme chooses a lighter one — white on
   * the dark theme's Signal Orange measures 3.32:1, below the AA floor, on
   * every primary action in the app at once. */
  --on-accent:#ffffff;
  --on-accent-wash:rgba(255,255,255,.22); /* the count chip on an active tab */

  /* The active sub-nav row's wash. This was --accent written out as raw
   * channels — rgba(45,85,212,.24) — so it silently went stale the moment
   * any theme re-pointed the accent.
   *
   * *** ITS LABEL TAKES --subnav-on-tx, NOT --on-accent, AND THE DIFFERENCE
   * IS NOT PEDANTRY. *** --on-accent is ink for a SOLID accent fill. This
   * row is a 22% wash over the rail, which is a different surface and in a
   * dark theme a much darker one: --on-accent there is #101010, which on
   * the composited wash measures 1.48:1 — the label telling you which page
   * you are on, invisible. Every other var(--on-accent) in this file sits
   * on background:var(--accent); this row is the one that does not. */
  --subnav-on:rgba(225,29,72,.24);
  --subnav-on-tx:#ffffff;  /* the label   — 14.26:1 on the default wash */
  --subnav-on-ct:#ccd8f5;  /* the count beside it */

  --sidebar:#0e1322;    /* deep navy console rail — a step darker, so the brand band reads */
  --sidebar-ink:#b3bed4;
  --sidebar-ink2:#7683a3;   /* lifted from #6b7799 — it labels real nav sections */

  /* ---- the ACTION colour language -----------------------------------
   * Rebuilt on one construction rather than eight independent guesses:
   * every -bg sits in a narrow lightness band so no pill shouts louder
   * than another for reasons unrelated to its meaning, and every -tx
   * clears 4.5:1 on its own -bg. Ordered here as the WATERFALL runs, so
   * this block doubles as the canonical statement of that order. */
  --input-bg:#e6eeff;    --input-tx:#1c4694;   /* INPUT PRODUCT — not listed at all */
  --sec-bg:#f0e9fb;      --sec-tx:#5c3596;     /* INPUT PRODUCT (SECONDARY) */
  --activate-bg:#e2f2f9; --activate-tx:#135b76;/* ACTIVATE LISTING */
  --stock-bg:#fde9e3;    --stock-tx:#97341a;   /* UPDATE STOCK */
  --price-bg:#fcecda;    --price-tx:#8a500d;   /* UPDATE PRICE */
  --ok-bg:#e3f3e9;       --ok-tx:#14663a;      /* OK — nothing to do */
  --nr-bg:#edeff3;       --nr-tx:#515b6b;      /* NOT REQUIRED — outside the ramp */
  --hold-bg:#dcf1ec;     --hold-tx:#0e5d50;    /* HELD (LOW STOCK) — a stock floor is keeping it off sale */
  --urgent-bg:#fce9e9;   --urgent-tx:#8a1f1f;  /* refusals and failures */
  /* The price-gap BOTTOM-zone bar (2 Oct 2026): a SOLID red with white ink,
   * meant to look bad (owner), and caution-tape stripes. */
  --shame-bg:#b3121b;    --shame-tx:#ffffff;   --shame-tape-a:#f5c400; --shame-tape-b:#161616;
  /* Applied rules' Map tiles: bright tint, ink, strong edge (1 Oct 2026). */
  --map-prim-bg:#d3f5de;  --map-prim-tx:#0d5c2b;  --map-prim-edge:#16a34a;
  --map-sec-bg:#ffe7c2;   --map-sec-tx:#7a3d00;   --map-sec-edge:#f59e0b;
  --map-skip-bg:#e3e8f0;  --map-skip-tx:#334155;  --map-skip-edge:#64748b;
  --map-black-bg:#ffd9d9; --map-black-tx:#8a1111; --map-black-edge:#dc2626;
  --map-on-bg:#e0e5ff;    --map-on-tx:#2f2a85;    --map-on-edge:#4f46e5;

  /* The BORDER that goes with each of those fills. Four rules carried
   * these as literals that no theme could reach, so a dark theme would
   * have drawn a pastel pink edge around a near-black alert.
   *
   * --input-line exists rather than letting .alert-pending and
   * .rl-flag-shadow keep borrowing --accent-line: they are blue because
   * the INPUT PRODUCT family is blue, not because they are the accent, and
   * in a theme whose accent is orange that borrowing puts an orange edge
   * on a blue alert. Same value today, so nothing moves now. */
  --urgent-line:#f3cfcf; --ok-line:#bde0cd;
  --price-line:#f0d9b0;  --input-line:#c5d3f8;

  /* One step darker, for the one place an urgent border sits INSIDE an
   * urgent panel rather than around one: .rf-confirm-in, the typed
   * confirmation between an unreviewed rule and thousands of rows. Against
   * --urgent-bg the ordinary --urgent-line is 1.23:1 and the well loses its
   * edge; this is 1.58:1. Folding the two together was a real regression
   * caught in review — an input you cannot see the boundary of is not an
   * input, and this is the last thing standing before a destructive write. */
  --urgent-line-strong:#e0b4b4;

  /* The saturated MARKER hues — the sidebar dots, the bar segments, the
   * coloured edge on an action tile. Lifted out of the .p-*-d rules below
   * so a hue is written ONCE and "the same hue everywhere" is structurally
   * true rather than true by convention.
   *
   * *** THESE ARE SHAPES, NEVER TEXT BACKGROUNDS. *** White on --price-dot
   * measures 2.35:1 — it fails AA outright. If you want a saturated tile,
   * use the -tx colour as the ground (white on --price-tx is 6.5:1), or put
   * these on a 3px edge and keep the tint ground. */
  --input-dot:#6d94f7; --sec-dot:#a67ddb;   --activate-dot:#48aed8;
  --stock-dot:#e2634a; --price-dot:#e39a3d; --ok-dot:#43ad68;
  --nr-dot:#7c88a0;    --hold-dot:#2c9d88;

  /* ---- the MARKETPLACE hues, and the completion ramp ------------------
   * Everything below used to be a literal hex somewhere a stylesheet could
   * not reach — three of them inline in Blade (`style="background:#ee6c2b"`),
   * which beats any stylesheet and made these the only elements in the app
   * that could not be restyled. They are tokens now so an alternate theme
   * can override them; see the [data-theme] block at the end of this file.
   *
   * UNIFIED, and worth recording that it WAS a drift: the four platform
   * badges carried two different pairs (#ee6c2b/#3a9e6b in three views,
   * #d95e21/#2f8f5f in the division dashboard) while this file's own
   * .plat-badge default was a third state. Standardised on the darker pair,
   * which is what .plat-badge already said and is the better of the two on
   * contrast. Shopee orange / Tokopedia green are the platforms' own brand
   * hues — that is the whole point of the badge, so they are NOT part of the
   * action language above and never mean an action.
   *
   * THEN DARKENED FOR CONTRAST (10 Sep 2026), which is a deliberate drift
   * from those brand hues and the reason it is written down. The badge is
   * 12px at weight 700 — under WCAG that is body text, not "large text"
   * (which starts at 18.66px bold), so white on it must clear 4.5:1 and did
   * not: #d95e21 was 3.77:1 and #2f8f5f was 4.03:1. Both channels were
   * scaled uniformly in sRGB — #d95e21 x0.891, #2f8f5f x0.926 — so the hue
   * is preserved and only the value moves; the shift is barely perceptible
   * beside the old pair. They now measure 4.61:1 and 4.62:1.
   *
   * The target was 4.6:1 rather than a bare 4.5:1 on purpose: sitting exactly
   * on the threshold means the next nudge, in either direction, silently
   * fails it. tests/Feature/PaletteContrastTest.php computes these ratios
   * from this file and fails below 4.5:1, in both themes. */
  --plat-ols:#c1541d;   /* Shopee    — was #d95e21 */
  --plat-olt:#2c8458;   /* Tokopedia — was #2f8f5f */
  --plat-ink:#fff;
  /* The badge ITSELF (design prototype, 26 Sep 2026): a tint and an ink per
   * platform, like an action pill, rather than a solid block — thirty solid
   * orange blocks down the division table were the loudest thing on it. The
   * hue still says which platform. Shopee 6.12:1, Tokopedia 5.66:1;
   * PaletteContrastTest measures these pairs, in every theme. */
  --plat-ols-bg:#fdeee4; --plat-ols-tx:#9a3c0e;
  --plat-olt-bg:#e3f4ea; --plat-olt-tx:#1f6b45;

  /* The completion traffic light: 0-50 red, 51-79 yellow, 80-100 green
   * (Stage3_WebApp_Design.md). Each is a two-stop gradient in the default
   * theme; a theme that forbids gradients sets both stops to the same
   * value. */
  --bar-red-a:#e2634a;    --bar-red-b:#cf4028;
  --bar-yellow-a:#efba43; --bar-yellow-b:#dd9a1c;
  --bar-green-a:#6fd196;  --bar-green-b:#3fae68;

  /* The trough a stacked/segmented bar sits in: an ink wash on the paper
   * hero, a white wash on the dark theme's — exactly the kind of inversion
   * a token exists for. */
  --bar-trough:rgba(10,20,45,.07);

  /* The signed-out backdrop. */
  --login-bg:radial-gradient(circle at 15% 20%,rgba(225,29,72,.38),transparent 42%),radial-gradient(circle at 85% 85%,rgba(147,51,234,.38),transparent 45%),linear-gradient(135deg,#0e1322,#1b2340);

  /* ---- the two hero panels (.dash-hero, .ss-primary) ----------------
   * The only components built on a ground a theme REPLACES: navy here,
   * paper in colourblind, warm near-black in dark. Their text was literal
   * hex, so each theme had to re-override a dozen selectors one by one
   * (and HeroPanelThemeTest existed to catch a forgotten one). As tokens
   * (24 Sep 2026) a theme sets eight values and every rule follows —
   * including one added next month.
   *
   * *** PAPER, NOT NAVY, SINCE 26 Sep 2026 (owner: "make it visually clean
   * and aesthetically pleasing"). *** The navy block was the heaviest thing
   * on every page it sat on and fought the navy rail beside it; on paper the
   * page reads as one surface and the green completion figure, not the
   * block around it, is what the eye lands on. Every value is an existing
   * text token on --panel, so each pair is one PaletteContrastTest already
   * measures. The old navy was linear-gradient(180deg,#1b2436,#131a2b) with
   * #fff / #74dc9b / #c6d0e0 / #8e9ab3 on it — eight lines to put it back. */
  --hero-bg:var(--panel);
  --hero-edge:var(--line);
  --hero-ink:var(--ink);         /* the figures in the store summary */
  --hero-good:var(--ok-tx);      /* the completion figure */
  --hero-label:var(--ink2);      /* captions, row labels, the pile-bar key */
  --hero-num-alt:var(--ink2);    /* the second, smaller hero figure */
  --hero-faint:var(--ink3);      /* the foot line */
  --hero-line:var(--line2);

  /* ---- type ----------------------------------------------------------
   * Seven steps. There were fifteen distinct sizes before (10.5, 11, 11.5,
   * 12, 12.5, 13, 13.5, 14, 14.5, 15, 15px-bold, 16, 17, 18, 20, 26) —
   * which is not a scale, it is the absence of one, and it is most of why
   * the old screens read as unconsidered rather than any single wrong
   * choice. Half-pixel sizes are gone: they round unpredictably and bought
   * nothing.
   *
   * NO THIRD-PARTY FONT REQUEST, deliberately: this is a single-machine
   * tool that must work with the network unplugged. That rule used to mean
   * "the system stack only"; since 26 Sep 2026 the default theme sets Plus
   * Jakarta Sans, SELF-HOSTED (public/fonts, one 27KB variable file, SIL
   * OFL), which keeps the property the rule protected — nothing leaves the
   * machine — and gives the brand a face. The system stack stays as the
   * fallback, and Colourblind and Dark keep their own Geist. */
  /* 12px, not 11, since 26 Sep 2026 (UI review): the token had spread past
   * badges to text people READ — table headers, product rows, form labels —
   * and the dark theme had already raised it. 12px is the floor for that. */
  --fs-micro:12px;   /* badge counts, nav section labels, table headers */
  --fs-xs:12px;      /* captions, footnotes, helper text */
  --fs-sm:13px;      /* secondary text, dense table meta */
  --fs-base:14px;    /* body and table default */
  --fs-md:15px;      /* panel headings, the store picker */
  --fs-lg:22px;      /* page title */
  --fs-xl:26px;      /* card values, and the action-tile numerals */

  /* An EIGHTH step, and the only one. Used ONCE per page: the completion
   * figure on the store dashboard.
   *
   * It exists because the hierarchy only works with three levels — hero
   * number >> tile number >> label. --fs-xl is already the tile numeral, so
   * without a step above it the hero and the tiles read as equals and the
   * page has no single answer to "how am I doing?". Do not reach for this
   * anywhere else; a second use makes it a size rather than a role, and the
   * seven-step scale above stops being a scale again. */
  --fs-hero:40px;

  --font:"Plus Jakarta Sans","Segoe UI Variable Text","Segoe UI",system-ui,-apple-system,sans-serif;
  /* Mono is not a style choice here, it is what makes a KODEBARANG
   * readable. 'PR-SSD-SN-990-2T' is a segmented identifier, and in a
   * proportional face its segments land in a different place on every row,
   * so scanning 50 of them means reading 50 of them. In mono the segments
   * form columns and the eye can skip. Same reason every number in this
   * app is tabular. */
  --font-mono:"Cascadia Mono",Consolas,"SF Mono",ui-monospace,monospace;

  /* ---- spacing -------------------------------------------------------
   * A 4px grid. There were eighteen distinct padding values before. */
  --sp-1:4px; --sp-2:8px;  --sp-3:12px; --sp-4:16px;
  --sp-5:20px; --sp-6:24px; --sp-8:32px; --sp-10:40px;

  --radius:12px;
  --radius-sm:8px;
  --radius-xs:6px;

  /* One focus treatment for the whole app, defined once. See the
   * :focus-visible block near the bottom for why it is a ring rather than
   * a border change. */
  /* ORDER MATTERS: the first shadow is painted on top, so the widest one
   * is the band that stays visible. This was `3px accent-soft, 1px accent`
   * — the pale halo covered the accent line and left 1.17:1 on white, a ring
   * nobody could see (24 Sep 2026). A 2px panel-coloured gap inside a 2px
   * accent band reads on any ground. PaletteContrastTest measures it. */
  --focus-ring:0 0 0 2px var(--panel),0 0 0 4px var(--accent);
  /* The rail's own ring: light on the navy rail. A theme with a LIGHT rail
   * (Colourblind) sets a dark one — the literal white it used to be was
   * about 1.03:1 on #fafafa. */
  --rail-focus:rgba(255,255,255,.65);

  /* The channels every drop shadow in this file is mixed from. Kept as raw
   * channels rather than a colour because each of the five rules that uses
   * it carries its own alpha. A dark theme re-points this to pure black —
   * a navy shadow on a near-black page is muddy rather than invisible. */
  --shadow-rgb:10,20,45;

  /* The resting lift of a surface on the page: a hairline shadow under the
   * border, so a panel sits ON the canvas rather than being drawn onto it.
   * Deliberately faint — it separates, it does not float. One value, so
   * every panel, card and tile sits at the same height. */
  --elev:0 1px 2px rgba(var(--shadow-rgb),.04),0 2px 8px rgba(var(--shadow-rgb),.04);

  font-family:var(--font);
}

*{box-sizing:border-box;margin:0;padding:0}
body{
  background:var(--bg);color:var(--ink);
  font-size:var(--fs-base);line-height:1.5;
  -webkit-font-smoothing:antialiased;
  /* Tabular figures app-wide. Every number on every screen is either a
   * count, a price or a percentage that sits in a column next to another
   * one — there is no prose here whose numbers want proportional widths. */
  font-variant-numeric:tabular-nums;
}

/* Skip link — the rail is nine nav items deep (not counting section labels
 * or the locked placeholder, none of which are tab stops — see
 * layouts/app.blade.php for the up-to-date count), so a keyboard user would
 * otherwise tab through all of it on every single page load before
 * reaching the thing they came for. Visually hidden until focused. */
.skip-link{
  position:absolute;left:var(--sp-2);top:-56px;z-index:100;
  background:var(--panel);color:var(--accent);
  padding:var(--sp-2) var(--sp-4);border-radius:var(--radius-sm);
  font-size:var(--fs-sm);font-weight:600;text-decoration:none;
  box-shadow:0 4px 16px rgba(var(--shadow-rgb),.2);
  transition:top .12s ease;
}
.skip-link:focus{top:var(--sp-2)}

/* ---------- login ---------- */
#login{min-height:100vh;background:var(--login-bg);display:flex;align-items:center;justify-content:center}
/* A field's own error, under it (components/field-error.blade.php). */
.field-error{color:var(--urgent-tx);font-size:var(--fs-xs);margin-top:var(--sp-1)}
/* min(): 380px plus 40px of padding overflowed a narrow phone (UI review,
 * 26 Sep 2026); the card now never exceeds the screen less a 16px margin. */
.login-card{background:var(--panel);border-radius:var(--radius);padding:var(--sp-10);width:min(380px,calc(100vw - 32px));box-shadow:0 20px 60px rgba(var(--shadow-rgb),.35)}
.brand{display:flex;align-items:center;gap:var(--sp-3);margin-bottom:var(--sp-1)}
.brand-mark{width:30px;height:30px;border-radius:var(--radius-sm);background:var(--accent);color:var(--on-accent);display:flex;align-items:center;justify-content:center;flex-shrink:0}
.brand-mark svg{width:17px;height:17px}
.brand-name{font-weight:700;font-size:17px;letter-spacing:-.2px;line-height:1.15}
/* The rest of the name under the wordmark — see components/brand-name. */
.brand-sub{display:block;font-size:var(--fs-xs);font-weight:600;letter-spacing:0;opacity:.8}
.login-sub{color:var(--ink3);font-size:var(--fs-sm);margin-bottom:var(--sp-6)}
.field{margin-bottom:var(--sp-4)}
.field label{display:block;font-size:var(--fs-xs);color:var(--ink2);margin-bottom:var(--sp-1);font-weight:600}
.field input,.field select{width:100%;padding:10px var(--sp-3);border:1px solid var(--line);border-radius:8px;font-size:var(--fs-base);color:var(--ink);background:var(--control-bg);font-family:inherit}
.login-note{font-size:var(--fs-xs);color:var(--ink3);margin-top:var(--sp-1)}
/* A STACKED FORM — label over a full-width field — outside the sign-in
 * card too. /register and the /settings password card wrote `.fld` and got
 * bare browser inputs: `.fld` was only ever styled inside `.addbar` and
 * `.rf-row`, which lay it out their own way and are deliberately not
 * touched here. `.form-stack` caps the width, because a password field
 * stretched across a 1,500px panel reads as a search bar. */
.form-stack{max-width:380px}
.login-card .fld,.form-stack .fld{margin-bottom:var(--sp-4)}
.login-card .fld > label,.form-stack .fld > label{display:block;font-size:var(--fs-xs);color:var(--ink2);margin-bottom:var(--sp-1);font-weight:600}
.login-card .fld > input,.login-card .fld > select,.login-card .fld > textarea,
.form-stack .fld > input,.form-stack .fld > select{width:100%;padding:10px var(--sp-3);border:1px solid var(--line);border-radius:8px;font-size:var(--fs-base);color:var(--ink);background:var(--control-bg);font-family:inherit}
/* THE PRIMARY BUTTON SIZES TO ITS LABEL (24 Sep 2026). It was width:100%
 * because it was born on the sign-in card, which is the one place a
 * full-width button is right; everywhere else ("Add account", "Change
 * password") it stretched across the whole panel beside compact buttons. */
.btn{background:var(--accent);color:var(--on-accent);border:none;padding:11px var(--sp-4);border-radius:8px;font-size:var(--fs-base);font-weight:600;cursor:pointer;width:auto;font-family:inherit}
.login-card .btn{width:100%}
.btn:hover{background:var(--accent-hover)}
/* A press that looks like a press (UI review, 26 Sep 2026): no button in the
 * app had an :active state, so a click gave no feedback until the server
 * answered. A filter, not a colour, so every theme and variant follows. */
.btn:active:not(:disabled),.btn-sm:active:not(:disabled){filter:brightness(.92)}

/* ---------- app shell ---------- */
#app{display:none;min-height:100vh}
.shell{display:flex;min-height:100vh}
/* The rail STAYS PUT while the page scrolls.
 *
 * Three properties do it together and removing any one breaks it:
 * position:sticky + top:0 is the pinning; height:100vh gives it a
 * viewport-sized box; align-self:flex-start stops the flex container's
 * default `stretch` from sizing it to the WHOLE document, which leaves a
 * sticky element no room to move within its container and so does nothing
 * at all. It is sticky rather than position:fixed so the rail keeps
 * reserving its own 210px of the flex row -- fixed would take it out of
 * flow and .main would slide underneath it.
 *
 * This also fixes something that was never right: stretched to document
 * height, the .who block (name, scope, Sign out) sat at the bottom of the
 * PAGE, so on a long worklist you scrolled a thousand rows to sign out.
 * At 100vh it sits at the bottom of the screen, which is where it always
 * looked like it was.
 *
 * THE RAIL ITSELF DOES NOT SCROLL -- .nav does, and the difference is not
 * cosmetic. Measured on /reconciliation in an 820px window: the rail wants
 * 960px, because that page opens a sub-menu and the nav is 19 links deep.
 * With the scroller on .rail, .who went to 873px -- Sign out BELOW the
 * fold, reachable only by scrolling inside the sidebar, which is worse
 * than what it replaced. Scrolling only the nav keeps .brand pinned to the
 * top of the screen and .who pinned to the bottom however long the list
 * gets, which is what a sidebar is supposed to do.
 *
 * The rail is display:none below 820px (see the breakpoint further down),
 * so none of this applies on a phone. */
.rail{
  width:210px;background:var(--sidebar);color:var(--sidebar-ink);
  display:flex;flex-direction:column;flex-shrink:0;
  position:sticky;top:0;align-self:flex-start;
  height:100vh;overflow:hidden;
}
.rail .brand{padding:var(--sp-5) var(--sp-5) var(--sp-4);border-bottom:1px solid rgba(255,255,255,.07);margin-bottom:0}
.rail .brand-name{color:#fff}
/* min-height:0 is load-bearing, not defensive: a flex item's default
 * min-height is `auto`, which refuses to shrink below its content, so
 * overflow-y:auto here would never get a box short enough to scroll and the
 * nav would simply push .who off the bottom of the rail. */
.nav{padding:var(--sp-3) 10px;flex:1;min-height:0;overflow-y:auto}
.nav-label{font-size:var(--fs-micro);color:var(--sidebar-ink2);padding:var(--sp-3) 10px var(--sp-1);letter-spacing:.06em;text-transform:uppercase;font-weight:600}
.nav a{display:flex;align-items:center;gap:10px;padding:9px 11px;border-radius:var(--radius-sm);color:var(--sidebar-ink);text-decoration:none;font-size:var(--fs-sm);cursor:pointer;margin-bottom:1px}
.nav a:hover{background:rgba(255,255,255,.06);color:#fff}
.nav a.on{background:var(--accent);color:var(--on-accent);font-weight:600}
/* Icons are inline SVG now. They were geometric Unicode characters
 * (U+25A7 ▧, U+25A4 ▤, U+25A6 ▦, U+25C8 ◈), which had two problems beyond
 * looking like placeholders: neither Segoe UI nor Inter covers those
 * codepoints at every weight, so they fell through to whatever face did
 * and rendered at a mismatched size and baseline; and with no aria-hidden
 * a screen reader announced "square with upper left to lower right fill"
 * before every nav label. */
.nav a .ico{width:16px;height:16px;flex-shrink:0;display:block;opacity:.9}
.nav a.on .ico{opacity:1}

/* Reconciliation sub-menu — the waterfall, drawn as one.
 *
 * THE SIGNATURE ELEMENT OF THIS DESIGN, and the one place any boldness is
 * spent. These seven links are not a menu of unrelated views: they are the
 * ordered cascade every row falls through, and the order carries real
 * information a newcomer cannot guess (a product is checked for "is it
 * listed at all" long before "is the price right"). The spine below draws
 * that sequence as a single descent instead of seven separate items, and
 * the dots sit ON it. Everything else in this design stays quiet so this
 * can be the thing the screen is remembered by. */
.subnav{margin:2px 0 var(--sp-2) 0;padding-left:var(--sp-4);position:relative}
.subnav::before{
  content:"";position:absolute;left:15px;top:10px;bottom:10px;width:1px;
  background:linear-gradient(180deg,
    rgba(255,255,255,.03),
    rgba(255,255,255,.16) 12%,
    rgba(255,255,255,.16) 88%,
    rgba(255,255,255,.03));
}
.subnav a{padding:var(--sp-1) 10px var(--sp-1) var(--sp-3);font-size:var(--fs-xs);color:var(--sidebar-ink);display:flex;align-items:center;gap:10px;border-radius:var(--radius-xs);cursor:pointer;margin-bottom:0;position:relative}
.subnav a:hover{background:rgba(255,255,255,.05);color:#fff}
.subnav a.on{background:var(--subnav-on);color:var(--subnav-on-tx);font-weight:600}
.subnav .dot{
  width:8px;height:8px;border-radius:50%;flex-shrink:0;position:relative;
  /* A ring in the rail's own colour so the dot reads as sitting ON the
   * spine rather than being crossed out by it. */
  box-shadow:0 0 0 3px var(--sidebar);
}
/* The dot classes now READ the marker tokens rather than repeating their
 * values, so the sidebar dot, the pile-bar segment and the action tile's
 * edge cannot drift apart. Rendering is byte-identical. */
.p-input-d{background:var(--input-dot)}.p-price-d{background:var(--price-dot)}.p-stock-d{background:var(--stock-dot)}
.p-sec-d{background:var(--sec-dot)}.p-activate-d{background:var(--activate-dot)}.p-nr-d{background:var(--nr-dot)}.p-ok-d{background:var(--ok-dot)}
.p-hold-d{background:var(--hold-dot)}

.rail .who{padding:var(--sp-3) var(--sp-4);border-top:1px solid rgba(255,255,255,.07);font-size:var(--fs-xs)}
.who .name{color:#fff;font-weight:600}
.who .scope{color:var(--sidebar-ink2);font-size:var(--fs-micro);margin-top:1px}
.who .out{color:var(--sidebar-ink);font-size:var(--fs-xs);margin-top:var(--sp-2);cursor:pointer;display:inline-block;border-radius:var(--radius-xs)}
.who .out:hover{color:#fff}
/* Sign out is a <button> (it POSTs), reset to read as the link it replaced. */
.who button.out{background:none;border:none;padding:0;font:inherit}

.main{flex:1;display:flex;flex-direction:column;min-width:0}
.topbar{background:var(--panel);border-bottom:1px solid var(--line);padding:var(--sp-4) var(--sp-6);display:flex;align-items:center;justify-content:space-between;gap:var(--sp-4)}
.topbar h1{font-size:var(--fs-lg);font-weight:700;letter-spacing:-.3px}
/* A short fact beside the page title (the division dashboard's store count). */
.title-badge{display:inline-block;vertical-align:middle;margin-left:var(--sp-3);padding:2px var(--sp-3);border-radius:999px;background:var(--brand-fill);color:var(--on-accent);font-size:var(--fs-sm);font-weight:700;letter-spacing:0;font-variant-numeric:tabular-nums}
.div-btn-n{margin-left:var(--sp-2);padding:0 7px;border-radius:999px;background:var(--brand-fill);color:var(--on-accent);font-size:var(--fs-micro);font-weight:700;font-variant-numeric:tabular-nums}
.div-btn.on .div-btn-n{background:var(--panel);color:var(--accent)}
.topbar .sub{color:var(--ink3);font-size:var(--fs-xs);margin-top:2px}
.fresh{font-size:var(--fs-xs);color:var(--ink3);text-align:right;flex-shrink:0}
.fresh b{color:var(--ink2);font-weight:600}
/* STORES THAT MISSED THE LATEST SNAPSHOT — the notification that replaced a
 * failed reconcile run. Urgent-coloured on purpose: this corner is a SCAN,
 * not a list of peers, and the /coverage precedent says loudness is allowed
 * to carry meaning on a scanning surface. It is absent on a normal day, so
 * when it is there it should be the thing your eye lands on. */
/* A BUTTON since 17 Sep 2026, not a span: it opens the panel below, which
 * names the stores and why each one missed the run. The declarations are
 * unchanged apart from the resets a button needs (a UA stylesheet gives it
 * its own font, and `cursor` is what says it can be pressed). */
.stale-pill{
  display:inline-flex;align-items:center;gap:6px;flex-shrink:0;
  background:var(--urgent-bg);color:var(--urgent-tx);
  border:1px solid var(--urgent-line);border-radius:20px;
  padding:3px 10px;font-size:var(--fs-micro);font-weight:650;white-space:nowrap;
  font-family:inherit;line-height:inherit;cursor:pointer;
}
.stale-pill:hover{background:var(--urgent-line)}

/* ===== STALE STORES PANEL
 * What the pill opens. Anchored to the pill rather than centred, because it
 * is about the thing it sits beside; the topbar is the last row before the
 * content, so it hangs down and to the LEFT (the pill is near the right edge,
 * and a right-aligned panel would leave the viewport). */
.stale-wrap{position:relative;display:inline-flex}
.stale-panel{
  position:absolute;top:calc(100% + 6px);right:0;z-index:60;
  width:min(420px, calc(100vw - 32px));
  max-height:min(60vh, 520px);overflow:auto;
  background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);
  box-shadow:0 8px 24px rgba(var(--shadow-rgb), .18);
  font-size:var(--fs-sm);color:var(--ink);text-align:left;white-space:normal;
}
.stale-panel-head{
  display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);
  padding:var(--sp-3) var(--sp-4);border-bottom:1px solid var(--line);
}
.stale-list{list-style:none;margin:0;padding:0}
.stale-list li{padding:var(--sp-3) var(--sp-4);border-bottom:1px solid var(--line)}
.stale-list li:last-child{border-bottom:0}
.stale-store{display:flex;align-items:baseline;gap:var(--sp-2);flex-wrap:wrap}
.stale-store a{font-weight:650}
.stale-age{color:var(--ink3);font-size:var(--fs-micro)}
.stale-why{margin-top:2px;color:var(--ink2);font-size:var(--fs-micro)}
.stale-why b{color:var(--urgent-tx)}
.stale-foot{
  padding:var(--sp-3) var(--sp-4);background:var(--inset);
  color:var(--ink3);font-size:var(--fs-micro);
}
/* The full warehouse list, behind a toggle. */
.stale-items{margin-top:var(--sp-2)}
.stale-more{
  font-family:inherit;font-size:var(--fs-micro);font-weight:600;
  background:none;border:0;padding:0;cursor:pointer;
  color:var(--accent);text-decoration:underline;
}
.stale-more:hover{color:var(--accent-hover)}
/* *** SCOPED, because this codebase has no global [x-cloak] rule. *** Without
 * it the attribute is inert and 201 warehouse codes flash on screen before
 * Alpine initialises — which is the thing the toggle exists to prevent. */
.stale-item-list[x-cloak]{display:none}
.stale-item-list{
  margin-top:var(--sp-2);padding:var(--sp-2);
  background:var(--inset);border:1px solid var(--line2);border-radius:var(--radius-sm);
  font-size:var(--fs-micro);color:var(--ink3);
  max-height:160px;overflow:auto;overflow-wrap:anywhere;
}
/* The row-level version, on the dashboard of the store it is about. The
 * pill says HOW MANY; this says "the numbers you are reading are old",
 * which is the half a count in a corner cannot do. */
.stale-note{
  display:flex;align-items:center;gap:var(--sp-2);
  background:var(--urgent-bg);color:var(--urgent-tx);
  border:1px solid var(--urgent-line);border-radius:var(--radius);
  padding:var(--sp-3) var(--sp-4);font-size:var(--fs-sm);margin-bottom:var(--sp-4);
}
.content{padding:var(--sp-6);flex:1;overflow:auto}
/* The freshness banner's wrapper. MUST NOT be `flex:1` — see the comment
 * in layouts/app.blade.php. It sits above .content in the same flex column,
 * and `flex:0 0 auto` is what keeps it the height of its own contents
 * instead of claiming an equal share of the page. */
.content-alert{padding:var(--sp-6) var(--sp-6) 0;flex:0 0 auto}

.view{display:none}
.view.on{display:block}

/* summary strip */
.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--sp-3);margin-bottom:var(--sp-5)}
/* Column counts as CLASSES, not inline styles (24 Sep 2026): an inline
 * grid-template-columns out-ranks the narrow-screen rule below, so four
 * cards stayed four across a phone. Four is the default; these are the rest. */
.cards.cols-2{grid-template-columns:repeat(2,1fr)}
.cards.cols-3{grid-template-columns:repeat(3,1fr)}
.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:var(--sp-4) var(--sp-5);box-shadow:var(--elev)}
.card .k{font-size:var(--fs-xs);color:var(--ink3);margin-bottom:var(--sp-2);font-weight:600}
.card .v{font-size:var(--fs-xl);font-weight:700;letter-spacing:-.5px;line-height:1.15}
.card .v.sm{font-size:var(--fs-lg)}
.card .d{font-size:var(--fs-micro);color:var(--ink3);margin-top:var(--sp-1)}

.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;box-shadow:var(--elev)}
/* Panels stacked straight in the page (Settings) had nothing between them;
 * block flow collapses this with any margin a page already sets. */
.content > .panel + .panel{margin-top:var(--sp-4)}
.panel-head{padding:var(--sp-3) var(--sp-5);border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);flex-wrap:wrap}
.panel-head h2{font-size:var(--fs-md);font-weight:650}
.panel-head .tools{display:flex;gap:var(--sp-2);align-items:center;flex-wrap:wrap}

.search{padding:7px 11px;border:1px solid var(--line);border-radius:var(--radius-sm);font-size:var(--fs-sm);width:200px;font-family:inherit;color:var(--ink);background:var(--control-bg)}
.filter{padding:7px 11px;border:1px solid var(--line);border-radius:var(--radius-sm);font-size:var(--fs-sm);background:var(--control-bg);font-family:inherit;color:var(--ink2)}

.btn-sm{background:var(--accent);color:var(--on-accent);border:none;padding:var(--sp-2) var(--sp-3);border-radius:var(--radius-sm);font-size:var(--fs-sm);font-weight:600;cursor:pointer;font-family:inherit;width:auto}
.btn-sm:hover:not(:disabled){background:var(--accent-hover)}
.btn-ghost{background:var(--control-bg);color:var(--accent);border:1px solid var(--line)}
.btn-ghost:hover:not(:disabled){background:var(--accent-soft);border-color:var(--accent-line)}
/* An IRREVERSIBLE action among ordinary ones (UI review, 26 Sep 2026: Delete
 * looked exactly like Set password, next to Retire). The urgent ink says what
 * it is and the gap keeps a slip of the mouse off it. */
.btn-ghost.btn-danger{color:var(--urgent-tx);border-color:var(--urgent-line);margin-left:var(--sp-3)}
.btn-ghost.btn-danger:hover:not(:disabled){background:var(--urgent-bg);border-color:var(--urgent-line)}

/* Disabled state. There was NO :disabled styling for any button before,
 * which mattered most on the pager: it correctly emits @disabled on page
 * one, so "Prev" was functionally dead while rendering identically to the
 * live "Next" — same white ground, same accent-blue text, same pointer
 * cursor. A control that looks available and does nothing when clicked
 * teaches people the app is broken. */
.btn:disabled,.btn-sm:disabled,.btn-ghost:disabled,.subtab:disabled{
  opacity:.45;cursor:not-allowed;
  background:var(--line2);color:var(--ink3);border-color:var(--line);
}

/* ---------- tables ---------- */
/* A table that is wider than its panel scrolls INSIDE the panel rather than
 * having its right-hand columns clipped off the edge. Measured on the
 * reconciliation worklist at a 861px content column: UPDATE PRICE lands at
 * exactly 861 and UPDATE STOCK at 882 — so the last two columns, Copy and
 * DONE, were already unreachable there before BS stock was added on
 * 16 Sep 2026 and took it to 958. Done is the tick staff press to record a
 * finished row, so this was not cosmetic.
 *
 * width:100% on the table below still makes it FILL a panel it fits in; the
 * container only does anything when it does not. */
.table-scroll{overflow-x:auto}
/* *** THE ACTIONS COLUMN STAYS PUT WHILE THE REST SCROLLS. ***
 * .table-scroll alone stops a wide table spilling out of its panel, which
 * is what was hiding Edit and Remove on the force list — but it moves the
 * problem rather than solving it: the controls are now reachable only if
 * you think to scroll a table sideways, and nobody thinks to do that on a
 * table that looks complete. Pinning the last column means every row shows
 * what you can DO to it at all times, and the eleven columns of detail
 * scroll underneath.
 *
 * Opt-in, never automatic: it is only right where the last column holds
 * controls. On a table whose last column is data — a price, a count — a
 * pinned column would cover the numbers beside it.
 *
 * The backgrounds are not decoration. A sticky cell floats OVER the cells
 * it passes, so a transparent one shows both at once; it needs the same
 * ground as the row it belongs to, hover included. */
.pin-actions th:last-child{position:sticky;right:0;background:var(--line2)}
.pin-actions td:last-child{
  position:sticky;right:0;background:var(--panel);
  border-left:1px solid var(--line2);
}
.pin-actions tbody tr:hover td:last-child{background:var(--row-hover)}
/* A full-width row inside a pinned table (the row-anchored message on
 * /management) is not an actions cell: it spans the table, so pinning it
 * does nothing but draw the pinned column's edge down its left side. */
.pin-actions td[colspan]:last-child{position:static;border-left:none}

/* *** A LONG TABLE KEEPS ITS HEADER IN VIEW (24 Sep 2026). ***
 * Pages go up to 300 rows (PageSize::OPTIONS) and by row 40 nobody knows
 * which number is M3 and which is M4. The header can only stick to a box
 * that scrolls VERTICALLY: `.panel{overflow:hidden}` and `.table-scroll`'s
 * horizontal overflow each become the sticky header's scroll container, so
 * a plain `thead th{position:sticky}` pins to a box that never moves and
 * silently does nothing. So the long SCANNING tables opt in with
 * `.table-tall`: a box capped a little under the viewport, scrolling both
 * ways, with the header pinned inside it. Opt-in because a nested scroll
 * area is a cost — worth paying on a 300-row list, not on a 6-row queue.
 * `position:relative` is load-bearing: without it the table's `.sr-only`
 * spans (position:absolute) take <body> as their containing block, escape
 * this box's clipping, and stretch the PAGE to the table's full height —
 * 1,400px of blank scroll below the app on /division-dashboard (25 Sep). */
.table-tall{max-height:calc(100vh - 180px);overflow:auto;position:relative}
.table-tall thead th{position:sticky;top:0;z-index:2}
/* The top-right corner is pinned both ways, so it must sit over both the
 * scrolling header and the pinned column. */
.table-tall.pin-actions thead th:last-child{z-index:3}
table{width:100%;border-collapse:collapse;font-size:var(--fs-sm)}
/* Headings in --ink2 (24 Sep 2026): --ink3 on --line2 is 4.39:1 in the
 * default theme, just under AA, on every table in the app. */
thead th{text-align:left;padding:10px var(--sp-3);color:var(--ink2);font-weight:600;font-size:var(--fs-micro);border-bottom:1px solid var(--line);background:var(--line2);white-space:nowrap;letter-spacing:.03em;text-transform:uppercase}
/* A row header (<th scope="row">) is a body cell too, and gets the same box.
   Unstyled, it rendered flush against the table edge with no rule under it —
   hit once on /data-management and again on both listing-rules tables. */
tbody td,tbody th{padding:10px var(--sp-3);border-bottom:1px solid var(--line2);vertical-align:middle}
tbody th{text-align:left}
tbody tr:last-child td,tbody tr:last-child th{border-bottom:none}
tbody tr:hover{background:var(--row-hover)}

/* A sortable header is a real <button> now — it used to be a bare <th
 * wire:click>, unreachable by keyboard and announced as plain text. The
 * button fills the cell so the whole header stays clickable. */
th.sortable{padding:0}
th.sortable > button{
  width:100%;display:flex;align-items:center;gap:var(--sp-1);
  padding:10px var(--sp-3);
  background:none;border:none;cursor:pointer;
  font:inherit;color:inherit;letter-spacing:inherit;text-transform:inherit;
  font-weight:600;text-align:left;
}
th.sortable.num > button{justify-content:flex-end}
th.sortable > button:hover{color:var(--ink);background:var(--hover-strong)}
/* :hover is listed here too, and must be. Without it the hover rule above
 * wins on specificity, so moving the cursor over the column that IS sorted
 * strips its accent colour and makes it look like the inactive ones —
 * exactly backwards. */
th[aria-sort] > button,
th[aria-sort] > button:hover{color:var(--accent)}
/* The direction arrow. Before this, clicking a header reshuffled every row
 * and NOTHING on the page said which column or direction had won — a plain
 * usability bug, not only an accessibility one. It stayed unfixable for a
 * while because the component never passed $sort/$direction to the view at
 * all; see App\Livewire\Recon\Worklist::render(). */
.sort-ind{font-size:var(--fs-micro);line-height:1;opacity:0;flex-shrink:0}
th.sortable > button:hover .sort-ind{opacity:.4}
th[aria-sort] .sort-ind{opacity:1}

.kode{font-family:var(--font-mono);font-size:var(--fs-xs);font-weight:600;color:var(--ink);letter-spacing:-.01em;white-space:nowrap}
/* nowrap (24 Sep 2026): a code broke at its hyphen — "JKT03-" over "OLS01"
 * on Price gaps — which reads as two codes and makes rows uneven. */
.spec{color:var(--ink2);font-size:var(--fs-xs)}
.num{text-align:right}
.muted{color:var(--ink3)}
/* For values that are a single atom and must never break mid-token —
 * store codes especially ("JKT03-OLS34" wrapping after the hyphen reads as
 * two separate values). */
.nowrap{white-space:nowrap}

/* ---------- the action pills ---------- */
.pill{display:inline-block;padding:3px 9px;border-radius:20px;font-size:var(--fs-micro);font-weight:600;white-space:nowrap;letter-spacing:.01em}
.p-ok{background:var(--ok-bg);color:var(--ok-tx)}
.p-input{background:var(--input-bg);color:var(--input-tx)}
.p-price{background:var(--price-bg);color:var(--price-tx)}
.p-stock{background:var(--stock-bg);color:var(--stock-tx)}
.p-nr{background:var(--nr-bg);color:var(--nr-tx)}
.p-hold{background:var(--hold-bg);color:var(--hold-tx)}
.p-sec{background:var(--sec-bg);color:var(--sec-tx)}
.p-urgent{background:var(--urgent-bg);color:var(--urgent-tx)}
.p-activate{background:var(--activate-bg);color:var(--activate-tx)}
/* THE TWO DIRECTIONS OF A PRICE GAP. Not new hues — both reuse pairs that
 * already exist in all three themes, because a fourth palette entry would
 * need a counterpart in each and these two already carry the right meaning.
 * Over is --urgent, because a listing priced ABOVE ours is losing sales
 * today; under is --price, the money amber the UPDATE PRICE pill already
 * uses, because it is losing margin on something that IS selling.
 *
 * The COLOUR IS NOT THE FACT. Every row carries the sign as a character and
 * the direction as sr-only words — /price-gaps is a scanning surface where
 * loudness is allowed to mean something (the /coverage precedent), but this
 * app's rule is that a colour never carries a fact on its own. */
.p-over{background:var(--urgent-bg);color:var(--urgent-tx)}
.p-under{background:var(--price-bg);color:var(--price-tx)}

.scope-tag{display:inline-block;padding:2px var(--sp-2);border-radius:var(--radius-xs);font-size:var(--fs-micro);font-weight:600;background:var(--line2);color:var(--ink2);white-space:nowrap}
.scope-tag.store{background:var(--input-bg);color:var(--input-tx)}
.scope-tag.div{background:var(--sec-bg);color:var(--sec-tx)}
.scope-tag.all{background:var(--ok-bg);color:var(--ok-tx)}

/* Row actions (Copy / Edit / Remove) are real <button>s now — they were
 * <span wire:click>, i.e. invisible to the keyboard and announced as plain
 * text. Styling is unchanged so the tables look the same. */
.rowact{
  color:var(--accent);cursor:pointer;font-size:var(--fs-xs);font-weight:600;
  background:none;border:none;padding:2px var(--sp-1);margin:0 -4px;
  font-family:inherit;border-radius:var(--radius-xs);
}
.rowact:hover{text-decoration:underline}
.rowact.del{color:var(--stock-tx)}
/* A 24px target at least (UI review, 26 Sep 2026): at 2px padding these
 * links were ~22px tall, under the floor for a tablet finger. */
.rowact{min-height:24px}
/* QUIET: a destructive link repeated down a dense list (Applied rules puts
 * one on every decider line) reads as a wall of red. Muted at rest, red the
 * moment the pointer or keyboard reaches it — still confirmed on click. */
.rowact.del.quiet{color:var(--ink3)}
.rowact.del.quiet:hover,.rowact.del.quiet:focus-visible{color:var(--stock-tx)}

/* ===== CONTROL-LIST SELECTION BAR
 * Appears only with rows ticked. It reads urgent because what it offers is
 * withdrawing hand-authored entries — the same act the workbook sync makes
 * somebody type a number for, and this asks for the number too. */
.cl-selbar{
  display:flex;align-items:center;gap:var(--sp-3);flex-wrap:wrap;
  margin:0 var(--sp-5) var(--sp-3);padding:var(--sp-3) var(--sp-4);
  background:var(--urgent-bg);border:1px solid var(--urgent-line);
  border-radius:var(--radius);font-size:var(--fs-sm);color:var(--urgent-tx);
}
.cl-selbar label{font-size:var(--fs-micro);color:var(--urgent-tx)}
/* *** OUTLINED, NOT A SOLID URGENT FILL, and the palette test is why. ***
 * A solid var(--urgent-tx) ground needs ink chosen PER THEME: that token is
 * #8a1f1f in the default and #ff9d96 in dark, so one colour cannot sit on
 * both. Reaching for --on-accent is the documented mistake — it is ink for a
 * solid ACCENT fill — and test_on_accent_ink_is_only_used_on_an_accent_fill
 * named this selector the moment it was written. An outline on var(--panel)
 * needs no new token and reads the same in all three. */
.cl-sel-remove{
  font-family:inherit;font-weight:650;cursor:pointer;
  padding:4px 12px;border-radius:var(--radius-sm);
  background:var(--panel);color:var(--urgent-tx);
  border:1px solid var(--urgent-line-strong);
}
.cl-sel-remove:hover{background:var(--urgent-bg)}
.cl-selnote{flex-basis:100%;font-size:var(--fs-micro);color:var(--ink2)}
/* "Move to" (2 Oct 2026): its own line under the remove controls, in ordinary ink, because moving is not the red action. */
.cl-move{flex-basis:100%;display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2);padding-top:var(--sp-2);border-top:1px solid var(--urgent-line)}
.cl-selbar .cl-move label{color:var(--ink2);font-weight:600}
.rowact:disabled{opacity:.5;cursor:not-allowed;text-decoration:none}

.banner{background:var(--accent-soft);border:1px solid var(--accent-line);border-radius:var(--radius);padding:var(--sp-3) var(--sp-4);font-size:var(--fs-sm);color:var(--input-tx);margin-bottom:var(--sp-4);display:flex;gap:10px;align-items:flex-start}
.banner .ic{font-weight:800}
/* THE HEADLINE STAYS, THE EXPLANATION FOLDS (24 Sep 2026). Two- and
 * three-line banners sat above the work on six screens, so the first
 * screenful was prose. The bold sentence is what a returning reader needs;
 * the rest opens on "How this works", on the same line. wire:ignore.self on
 * the element keeps it open across Livewire round trips — without it the
 * morph removes the `open` attribute the server never rendered. */
/* The reason box on an Approvals row: narrow, so the Reject button beside
 * it stays on the same line. */
.aq-reject-note{max-width:11rem;margin-right:var(--sp-1)}
.banner-more{display:inline}
.banner-more > summary{display:inline;cursor:pointer;color:var(--accent);font-weight:600;margin-left:var(--sp-1);list-style:none}
.banner-more > summary::-webkit-details-marker{display:none}
.banner-more > summary::after{content:" ▸"}
.banner-more[open] > summary::after{content:" ▾"}
.banner-more > summary:focus-visible{outline:none;box-shadow:var(--focus-ring);border-radius:4px}
/* The fold-out toggles are links in all but name; they now underline on
 * hover like one. */
.banner-more > summary:hover,.trend-table summary:hover{text-decoration:underline}
.banner-more > div{margin-top:var(--sp-1)}
/* A division's shop list, folded to its count (Divisions rules screen and
 * the warehouse division map). Opens BELOW the header row, where a list of
 * forty-seven codes has the width to wrap, rather than inline. */
.shop-list > summary{cursor:pointer;color:var(--accent);font-weight:600;list-style:none;display:inline}
.shop-list > summary::-webkit-details-marker{display:none}
.shop-list > summary::after{content:" ▸"}
.shop-list[open] > summary::after{content:" ▾"}
.shop-list > summary:focus-visible{outline:none;box-shadow:var(--focus-ring);border-radius:4px}
.shop-list > div{margin-top:var(--sp-1);text-align:left;max-width:60em}

/* add-entry inline form */
.addbar{background:var(--inset);border-bottom:1px solid var(--line);padding:var(--sp-3) var(--sp-5);display:none;gap:10px;align-items:end;flex-wrap:wrap}
.addbar.on{display:flex}
.addbar .fld{display:flex;flex-direction:column;gap:var(--sp-1)}
.addbar label{font-size:var(--fs-micro);color:var(--ink3);font-weight:600}
.addbar input,.addbar select{padding:7px 10px;border:1px solid var(--line);border-radius:var(--radius-sm);font-size:var(--fs-sm);font-family:inherit;color:var(--ink);background:var(--control-bg)}
.addbar input:disabled,.addbar select:disabled{background:var(--disabled-bg);color:var(--ink3);cursor:not-allowed}
.addbar .lookup{font-size:var(--fs-xs);color:var(--ok-tx);margin-top:2px;min-height:14px}
.addbar .lookup.err{color:var(--stock-tx)}

.foot-note{font-size:var(--fs-xs);color:var(--ink3);padding:var(--sp-3) var(--sp-5);border-top:1px solid var(--line2);display:flex;align-items:center;gap:var(--sp-3);flex-wrap:wrap}

/* ---------- store dashboard ---------- */
.store-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--sp-4);gap:var(--sp-4);flex-wrap:wrap}
.store-pick-wrap{display:flex;align-items:center;gap:10px}
.store-pick-wrap label{font-size:var(--fs-xs);color:var(--ink3);font-weight:600}
.store-pick{padding:var(--sp-2) var(--sp-3);border:1px solid var(--line);border-radius:8px;font-size:var(--fs-md);font-weight:650;color:var(--ink);background:var(--control-bg);font-family:inherit}
/* The platform badge takes its hue from a MODIFIER CLASS, never from an
 * inline style. `.is-ols`/`.is-olt` are set from Store::platform in the
 * Blade — see resources/views/livewire/store/dashboard.blade.php. The bare
 * .plat-badge keeps Shopee's hue as a fallback so a badge rendered without a
 * modifier is still legible rather than transparent. */
.plat-badge{background:var(--plat-ols-bg);color:var(--plat-ols-tx);font-weight:700;font-size:var(--fs-xs);padding:5px var(--sp-3);border-radius:var(--radius-xs);letter-spacing:.03em}
.plat-badge.is-ols{background:var(--plat-ols-bg);color:var(--plat-ols-tx)}
.plat-badge.is-olt{background:var(--plat-olt-bg);color:var(--plat-olt-tx)}
.store-fixed{font-size:var(--fs-md);font-weight:700;color:var(--ink);padding:var(--sp-1)}
.store-pick:disabled{background:var(--disabled-bg);color:var(--ink2);cursor:not-allowed;opacity:.9}
.store-summary{display:grid;grid-template-columns:1fr 1.4fr;gap:var(--sp-3);margin-bottom:var(--sp-5)}
.ss-block{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:var(--sp-2) var(--sp-1);box-shadow:var(--elev)}
.ss-row{display:grid;grid-template-columns:1fr auto 54px;align-items:center;gap:var(--sp-3);padding:9px var(--sp-5);font-size:var(--fs-sm)}
.ss-row span:first-child{color:var(--ink2)}
.ss-row b{font-weight:700;font-size:var(--fs-md);text-align:right}
.ss-pct{text-align:right;color:var(--ink3);font-weight:600}
.ss-row.ss-hi{border-top:1px solid var(--line2);margin-top:2px}
.ss-row.ss-hi b,.ss-row.ss-hi .ss-pct{color:var(--ok-tx)}
.ss-primary{background:var(--hero-bg)}
.ss-primary .ss-row span:first-child{color:var(--hero-label)}
.ss-primary .ss-row b{color:var(--hero-ink)}
.ss-primary .ss-row.ss-hi b,.ss-primary .ss-row.ss-hi .ss-pct{color:var(--hero-good)}
.ss-primary .ss-row.ss-hi{border-top-color:var(--hero-line)}

/* ---------- dashboard hero + action tiles ----------
 *
 * The idea, taken from a flat-admin template the owner liked and adapted to
 * this app's constraints: ONE number is the hero and is much bigger than
 * everything else, and each pile of work is a THING you can see and click
 * rather than a row you have to read.
 *
 * THE ADAPTATION THAT MATTERS. The reference gives its four tiles four
 * arbitrary hues because four hues look good. Here the tiles ARE the
 * actions, so they take the action hues — which means colour on this screen
 * says the same thing it says on the sidebar dot, the sub-tab and the pill
 * in the table. Arbitrary hues would have taught staff that colour is
 * decoration, and they would have carried that lesson to the worklist,
 * where the pill colour is the fastest read of "what do I do with this row".
 */
.dash-hero{
  background:var(--hero-bg);border:1px solid var(--hero-edge);
  border-radius:var(--radius);padding:var(--sp-5) var(--sp-6);
  display:flex;flex-direction:column;gap:var(--sp-3);box-shadow:var(--elev);
}
/* The ground is the panel's own (see the --hero-* tokens): the one figure
 * that is not an action is set apart by SIZE, not by a coloured block. */
.hero-figures{display:flex;align-items:baseline;gap:var(--sp-8);flex-wrap:wrap}
.hero-main .hero-num{font-size:var(--fs-hero);font-weight:750;color:var(--hero-good);line-height:1}
.hero-alt .hero-num{font-size:var(--fs-xl);font-weight:700;color:var(--hero-num-alt);line-height:1}
.hero-cap{font-size:var(--fs-xs);color:var(--hero-label);margin-top:var(--sp-1);max-width:26em}
.hero-foot{font-size:var(--fs-xs);color:var(--hero-faint);border-top:1px solid var(--hero-line);padding-top:var(--sp-3)}

/* The composition of the whole pile, drawn to scale — the reference's donut
 * done as a bar. Lengths compare better than angles, a zero segment simply
 * is not drawn (a zero donut slice has nowhere to put its label), and this
 * is six divs rather than arc trigonometry in Blade.
 *
 * *** THE DENOMINATOR IS TRACKED, NEVER TOTAL. *** Every percentage in this
 * app divides by tracked; a bar over total would make these segment widths
 * disagree with the percentages printed on the tiles two inches below. */
.pilebar{display:flex;height:12px;border-radius:6px;overflow:hidden;background:var(--bar-trough)}
.pilebar .seg{min-width:0}
/* The bar's key (components/pilebar-key.blade.php): a swatch and the pile's
 * name, in the hero's caption ink. The swatch reuses the segment's own -d
 * class, so it is the bar's colour in every theme by construction. */
.pilebar-key{display:flex;flex-wrap:wrap;gap:var(--sp-1) var(--sp-4);margin-top:var(--sp-2);font-size:var(--fs-micro);color:var(--hero-label)}
.pilebar-key span{display:inline-flex;align-items:center;gap:var(--sp-1)}
.pilebar-key i{display:inline-block;width:8px;height:8px;border-radius:2px}

.acttiles{display:grid;grid-template-columns:repeat(5,1fr);gap:var(--sp-3);margin:var(--sp-4) 0}
/* PAPER WITH A COLOURED EDGE, THE TINT ON HOVER (26 Sep 2026). Five tinted
 * grounds in a row read as a rainbow before they read as five numbers; on
 * paper the hue is carried by the edge and the numeral — still the action's
 * own colour, still the same language as the pill — and the tint comes back
 * under the pointer, where it says "this is the pile you are about to open".
 * Every -tx clears 4.5:1 on its -bg, and --panel is lighter than every -bg,
 * so no pairing got worse. */
.acttile{
  display:block;text-decoration:none;
  background:var(--panel);border:1px solid var(--line);border-top:3px solid var(--tile-dot);
  border-radius:var(--radius);padding:var(--sp-4);box-shadow:var(--elev);
}
.acttile:hover{background:var(--tile-bg)}
.at-label{display:flex;align-items:center;gap:var(--sp-2);font-size:var(--fs-xs);font-weight:600;color:var(--tile-ink)}
.at-num{font-size:var(--fs-xl);font-weight:750;color:var(--tile-ink);line-height:1.1;margin:var(--sp-2) 0 2px}
.at-sub{font-size:var(--fs-micro);color:var(--ink2)}
/* Tint ground + action ink, NOT saturated ground + white. Both are
 * accessible (7.65:1 and 6.5-8.9:1), but on a saturated ground every tile
 * shouts equally loudly whatever its number — so a violet block holding
 * 2.041 and a teal one holding 149 look the same weight, and the ranking
 * comes from hue, which is not a ranking. On tints the NUMERAL is the
 * loudest thing, which is the one idea the reference genuinely gets right. */
.a-input   {--tile-bg:var(--input-bg);   --tile-ink:var(--input-tx);   --tile-dot:var(--input-dot)}
.a-sec     {--tile-bg:var(--sec-bg);     --tile-ink:var(--sec-tx);     --tile-dot:var(--sec-dot)}
.a-activate{--tile-bg:var(--activate-bg);--tile-ink:var(--activate-tx);--tile-dot:var(--activate-dot)}
.a-stock   {--tile-bg:var(--stock-bg);   --tile-ink:var(--stock-tx);   --tile-dot:var(--stock-dot)}
.a-price   {--tile-bg:var(--price-bg);   --tile-ink:var(--price-tx);   --tile-dot:var(--price-dot)}

@media(max-width:1180px){ .acttiles{grid-template-columns:repeat(3,1fr)} }

/* category grid */
.catgrid{display:grid;grid-template-columns:1fr 1fr;gap:0 var(--sp-8);padding:var(--sp-3) var(--sp-5)}
/* One column below 1180px, the width Listing rules' .cat-catgrid already
 * collapses at (27 Sep 2026). At 820-1180px two halves of ~255px left the
 * category NAME ~8px wide beside the count, bar and menu: rows read
 * "35/138" with no name at all. */
@media(max-width:1180px){.catgrid{grid-template-columns:1fr}}
/* Holds one section's rows so x-show can toggle them all at once, while
 * display:contents keeps each row a direct grid item of .catgrid.
 *
 * A CLASS, deliberately — see the comment in category-drilldown.blade.php.
 * As an inline style Alpine's x-show wiped it on every show
 * (el.style.removeProperty('display')), the wrapper fell back to block, and
 * the two-column grid silently rendered as one.
 *
 * A SUBGRID, NOT display:contents, since 27 Sep 2026. Opening a category
 * drops its full-width brand panel straight after it, which used to push its
 * right-hand neighbour down and swap every later row to the other column, so
 * the reader lost their place. `dense` packing fills the gap beside the
 * opened row with the next category, which is the neighbour that sat there
 * before. It has to be one grid per SECTION: across the whole .catgrid, dense
 * would pull the next section's first category up into the last gap of this
 * one, above that section's heading. `subgrid` keeps the parent's columns and
 * gap, so rows still line up across sections at every width. */
.section-rows{
  display:grid;grid-column:1 / -1;grid-template-columns:subgrid;
  grid-auto-flow:row dense;
}
/* The "n categories hidden" note. Spans both grid columns, like .catsec. */
.catgrid-note{grid-column:1 / -1;font-size:var(--fs-xs);color:var(--ink3);padding:var(--sp-3) var(--sp-1) 0;border-top:1px solid var(--line2);margin-top:var(--sp-3)}
/* Explains what the category-tier filter is currently doing. Sits directly
 * under the panel head, above the grid — the reader needs to know which
 * question the percentages below are answering before reading them. */
.tier-note{font-size:var(--fs-xs);color:var(--ink2);background:var(--line2);padding:var(--sp-2) var(--sp-5);border-bottom:1px solid var(--line)}
/* .catsec is a <button> now (it was a <div @click>, keyboard-unreachable).
 * These resets make a button behave like the flex row the layout expects.
 *
 * .catrow and .brandrow went the OTHER way on 15 Sep 2026: they were
 * <button>s wrapping the whole row, and they are plain containers again now
 * that the row holds TWO controls — an arrow that expands and a name that
 * links to the worklist. A link inside a button is invalid HTML, so the
 * button had to give way rather than the link. The row keeps its reset
 * properties: they are harmless on a <div> and mean this rule does not have
 * to be re-derived if a row ever becomes a control again. */
/* ===== THE RULE MENU'S TRIGGERS AND PANEL
 *
 * A ⋮ on every row a listing rule can name — section, category and brand,
 * never a product, because no rule family accepts a PRODUCT target except
 * the carve-out (see App\Livewire\Rules\RuleQuickMenu). Right-click opens
 * the same panel; the button exists because right-click is unreachable by
 * keyboard, invisible until somebody is told, and absent on a tablet.
 *
 * REVEALED ON HOVER *AND* ON :focus-visible. Hover alone would make it
 * keyboard-invisible — the control would exist, be tabbable, and be painted
 * in nothing.
 *
 * *** IT IS `opacity`, AND IT SHIPPED AS `visibility`, WHICH MEANT NONE OF
 * THE ABOVE WAS TRUE. *** An element with `visibility:hidden` is removed
 * from sequential focus navigation in every browser: it can never take
 * focus, so `.rmenu-btn:focus-visible` could never match and that rule was
 * dead. With right-click the only other door, and right-click reachable
 * only by a mouse, the whole quick-rule feature was unreachable from the
 * keyboard — the exact failure the paragraph above was written to prevent,
 * in the line under it. `opacity:0` keeps the box (so no row reflows as the
 * pointer crosses, which is what `display:none` would have cost) and keeps
 * the button focusable. */
.catsec-wrap{grid-column:1 / -1;display:flex;align-items:stretch;gap:var(--sp-1)}
.catsec-wrap > .catsec{flex:1 1 auto;grid-column:auto}
.rmenu-btn{
  flex:0 0 auto;align-self:center;
  background:none;border:none;font-family:inherit;font-size:14px;line-height:1;
  color:var(--ink3);cursor:pointer;padding:2px 6px;border-radius:var(--radius-xs);
  opacity:0;
}
.catsec-wrap:hover .rmenu-btn,
.catrow:hover .rmenu-btn,
.brandrow:hover .rmenu-btn,
.rmenu-btn:focus-visible{opacity:1}
.rmenu-btn:hover{background:var(--hover-strong);color:var(--ink)}
/* THE TRIGGER GETS ITS OWN GRID COLUMN on .catrow and .brandrow (22px,
 * matching the button's own width). As a fourth child of a three-column
 * grid it wrapped onto an implicit SECOND ROW — doubling the height of
 * every category and brand row, visible or not, because a hidden button
 * still occupies its box (`opacity`, like the `visibility` this was
 * written against, hides without removing). The column is reserved
 * whether or not the button is drawn, so a staff render and a
 * supervisor's line up. */

/* The panel itself. Anchored where the pointer was; the view's place()
 * flips and clamps it inside the viewport once it has a measured height,
 * because a right-click on a row near the bottom of this very long list
 * otherwise opens something cut off at the fold. max-height is the last
 * resort under that — a panel taller than the window scrolls rather than
 * spilling, and its 16px is deliberately TWICE the `gap` place() keeps at
 * each edge: that is what makes place()'s bottom-pinned branch provably
 * non-overflowing. Change one of those two numbers and change the other.
 * z-index sits above the rule drawer's scrim (70/71) because this can be
 * opened from a page that has none. */
.rmenu-scrim{position:fixed;inset:0;z-index:80;background:transparent;cursor:default}
.rmenu{
  position:fixed;z-index:81;min-width:250px;max-width:min(320px,92vw);
  max-height:calc(100vh - 16px);overflow-y:auto;
  background:var(--panel);border:1px solid var(--line);
  border-radius:var(--radius-sm);box-shadow:0 10px 30px rgba(var(--shadow-rgb),.22);
  padding:var(--sp-2) 0;
}
.rmenu:focus{outline:none}
.rmenu-head{padding:var(--sp-1) var(--sp-4) var(--sp-2);border-bottom:1px solid var(--line2)}
.rmenu-head h3{font-size:var(--fs-sm);font-weight:700;line-height:1.3}
/* WHAT THE RULE WILL ACTUALLY SAY, which is not always what the row says —
 * a section row reading MOBILE writes at group PHONE. Muted because it is
 * the small print under the name, not a second heading. */
.rmenu-target{font-size:var(--fs-micro);color:var(--ink3);margin-top:2px}
.rmenu-state{padding:var(--sp-2) var(--sp-4) 0;font-size:var(--fs-xs);color:var(--ink2)}
.rmenu-note{padding:var(--sp-2) var(--sp-4);font-size:var(--fs-xs);color:var(--ink2);margin:0}
.rmenu-msg{margin:var(--sp-2) var(--sp-3);font-size:var(--fs-xs)}
.rmenu-group{padding:var(--sp-2) 0 0}
/* THE WIDER PRESS GETS ITS OWN BLOCK AND ITS OWN GROUND. Both scopes were
 * asked for; what must not happen is a division-wide write sitting flush
 * against a single-shop one, told apart only by the tail of a label. */
.rmenu-group.is-wide{border-top:1px solid var(--line2);margin-top:var(--sp-2);background:var(--inset)}
.rmenu-grouphead{
  padding:0 var(--sp-4) 2px;font-size:var(--fs-micro);font-weight:700;
  letter-spacing:.04em;text-transform:uppercase;color:var(--ink3);
}
.rmenu-item{
  display:flex;align-items:center;gap:var(--sp-2);width:100%;
  background:none;border:none;font-family:inherit;font-size:var(--fs-sm);
  color:var(--ink2);text-align:left;cursor:pointer;padding:var(--sp-2) var(--sp-4);
}
.rmenu-item:hover{background:var(--row-hover);color:var(--ink)}
.rmenu-item.on{color:var(--accent);font-weight:650}
/* Fixed width so every label starts at the same x whether or not its row is
 * the current one — a dot that shifts the text is a list that jitters. */
.rmenu-dot{flex:0 0 12px;font-size:var(--fs-micro);line-height:1}
/* THE STOCK FLOOR IS A ROW OF NUMBERS, not a list of sentences. Four
 * one-character choices read as a scale — you pick a position on it — where
 * four stacked rows reading "Need 2 in stock" would read as four unrelated
 * commands.
 *
 * *** THEY SHIPPED ON `--control-bg`, WHICH IS WHITE, ON A WHITE PANEL, AND
 * THE OWNER READ THEM AS UNCLICKABLE. *** They were never inert — real
 * buttons, bound, hit-testing clean — but a 30x23 box filled with the panel's
 * own colour behind a 0.57px hairline is a readout, not a control, beside
 * full-width rows that look like controls. So: the inset ground the app uses
 * for a toolbar sunk into a panel, a border that is actually visible, and
 * enough height to read as a target. The lesson is the cheap one — a control
 * that cannot be TOLD to be a control is as good as one that cannot act. */
.rmenu-chips{
  display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-1);
  padding:var(--sp-2) var(--sp-4) 0;
}
/* The words either side of the numbers, so the row reads as one sentence
 * the chip completes. Not a <label>: it labels no single control. */
.rmenu-chiplab{font-size:var(--fs-micro);color:var(--ink3)}
.rmenu-chip{
  /* --line, not --line2: the latter is the FAINT fill used for table heads
     and inset rows, and a border drawn in it on the inset ground is the
     hairline that made these look inert in the first place. */
  background:var(--inset);border:1px solid var(--line);font-family:inherit;
  font-size:var(--fs-sm);font-weight:650;color:var(--ink2);cursor:pointer;
  padding:4px 11px;border-radius:var(--radius-xs);min-width:34px;line-height:1.35;
}
.rmenu-chip:hover{background:var(--hover-strong);border-color:var(--ink3);color:var(--ink)}
.rmenu-chip.on{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
/* A COMPANY-WIDE PRESS TAKES ABOUT A SECOND — it replays every store in
 * scope, measured at 0.8s over 31. Without this the panel sits perfectly
 * still after a click, which reads as nothing having happened, which is the
 * other half of "it seems unclickable". */
.rmenu-group.is-busy,
.rmenu-chips.is-busy{opacity:.5;pointer-events:none}
.rmenu-foot{padding:var(--sp-2) var(--sp-4) 0;border-top:1px solid var(--line2);margin-top:var(--sp-2)}

.catsec{
  grid-column:1 / -1;display:flex;align-items:center;gap:var(--sp-2);width:100%;
  font-family:inherit;font-size:var(--fs-xs);font-weight:700;color:var(--ink2);
  letter-spacing:.05em;text-transform:uppercase;background:var(--line2);
  padding:var(--sp-2) var(--sp-3);border:none;border-radius:var(--radius-xs);
  margin:10px 0 var(--sp-1);cursor:pointer;user-select:none;text-align:left;
}
.catsec:hover{background:var(--sec-hover)}
.catsec .csec-arrow{font-size:var(--fs-micro);width:12px;flex-shrink:0}
.catsec .csec-name{flex:0 0 auto}
.catsec .csec-roll{margin-left:auto;font-weight:600;color:var(--ink2);text-transform:none;letter-spacing:0}
.catsec.coll{opacity:.72}
.catrow{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(64px,140px) 22px;align-items:center;gap:var(--sp-3);padding:var(--sp-1) var(--sp-1);font-size:var(--fs-sm);border-bottom:1px solid var(--line2);width:100%;text-align:left;font-family:inherit;background:none;border-left:none;border-right:none;border-top:none;color:inherit}
.catrow.clik{cursor:pointer}
.catrow.clik:hover{background:var(--row-hover)}
/* ---- THE OPEN ROW AND ITS BRAND PANEL ARE ONE OBJECT ----------------
 *
 * THE PROBLEM, which is a layout problem and not a decoration one: the
 * grid is TWO columns and .brandwrap spans both, so expanding a category
 * in the RIGHT column opens a panel that begins on the LEFT, under a
 * different category entirely. The panel's caption named its parent, and
 * that was still not enough to see at a glance which row had opened it.
 *
 * THE FIX IS ONE ACCENT, USED IN EXACTLY THREE PLACES: the rail down the
 * open row, the rail down the panel, and the panel's header strip. Only
 * ever ONE row is open per category name, so the accent is unambiguous —
 * find the one blue row, and the one blue panel below it is its.
 *
 * All of it is var(--accent*), so the colourblind theme (where accent is
 * near-black ink) inherits the whole relationship with no per-theme rule.
 * That is why the two literal-hex overrides this pair used to carry in the
 * theme block are gone rather than updated.
 *
 * The rail is an INSET BOX-SHADOW, not a border-left: .catrow is a grid
 * whose columns are already sized, and adding a real border here shifts
 * every label 3px sideways the moment a row opens. */
.catrow.open{background:var(--accent-soft);box-shadow:inset 3px 0 0 var(--accent);border-bottom-color:var(--accent-line)}
.catrow.open .cn{color:var(--accent);font-weight:600}
.catrow.open .crow-arrow{color:var(--accent)}
.catrow .cn{color:var(--ink2);display:flex;align-items:center;gap:var(--sp-1);min-width:0}
/* THE ARROW IS THE EXPAND CONTROL for the keyboard. The row around it stopped
 * being a button when the name became a link, but it still looked clickable
 * (.clik: pointer and hover wash) and did nothing, so since 27 Sep 2026 a
 * click on the row's EMPTY space toggles it too. The name still navigates and
 * the ⋮ still opens the rule menu (category-drilldown.blade.php). A 10px
 * glyph is not a hit target, so the button is padded out to a usable one
 * and pulled back by the same amount with a negative margin, which keeps
 * every label in the grid lined up exactly where it was.
 *
 * It is NOT given a larger font: the arrow has to stay the same size as the
 * one on a category with no breakdown (.ph, an invisible placeholder), or
 * rows with and without children would no longer align. */
.crow-arrow{
  font-size:var(--fs-micro);color:var(--ink3);flex-shrink:0;display:inline-flex;
  align-items:center;justify-content:center;width:12px;
}
button.crow-arrow{
  background:none;border:none;font-family:inherit;cursor:pointer;
  padding:6px;margin:-6px -6px -6px -6px;width:22px;border-radius:var(--radius-xs);
}
button.crow-arrow:hover{background:var(--row-hover);color:var(--ink2)}
.crow-arrow.ph{visibility:hidden}
/* The NAME, as a link to this slice of the worklist. Underlined only on
 * hover: at rest it has to read as the row label it has always been, or a
 * grid of 700 rows turns into a wall of blue. It inherits its colour so the
 * open-row accent below still reaches it. */
.cn-link{color:inherit;text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cn-link:hover{text-decoration:underline;color:var(--accent)}
.catrow .cf{color:var(--ink3);font-size:var(--fs-xs);text-align:right}
/* A panel, not a tinted region: its own paper, its own hairline, and the
 * accent rail that pairs it with the row above. Horizontal padding is 0 so
 * the header strip can run edge to edge; the rows re-indent themselves. */
.brandwrap{
  grid-column:1 / -1;background:var(--panel);
  border:1px solid var(--accent-line);border-left:3px solid var(--accent);
  border-radius:var(--radius-sm);margin:2px 0 var(--sp-2);padding:0 0 var(--sp-1);
}
/* Names the category this panel belongs to — see the block comment above
 * for why that is load-bearing here and not on any other drill-down. The
 * strip carries the same --accent-soft as the open row's background, so the
 * two halves of the pair are the same colour as well as the same rail. */
.brandwrap-head{
  display:flex;align-items:center;gap:var(--sp-1);
  background:var(--accent-soft);color:var(--accent);
  font-size:var(--fs-micro);font-weight:700;letter-spacing:.04em;text-transform:uppercase;
  padding:var(--sp-1) var(--sp-3);border-bottom:1px solid var(--accent-line);
  border-radius:calc(var(--radius-sm) - 1px) calc(var(--radius-sm) - 1px) 0 0;
}
/* Decorative, and deliberately in CSS rather than markup: it is a pointer
 * back up to the parent row, not a word anyone needs read aloud. */
.brandwrap-head::before{content:"\0021B3";font-size:12px;line-height:1;opacity:.75}
/* "— by brand" is scaffolding; the CATEGORY NAME is the thing being
 * matched back to a row above, so only it gets the full accent weight. */
.bw-tail{color:var(--ink3);font-weight:600}
.brandrow{display:grid;grid-template-columns:minmax(0,180px) 70px minmax(64px,140px) 22px;align-items:center;gap:var(--sp-3);padding:var(--sp-1) var(--sp-3);font-size:var(--fs-xs);border-bottom:1px solid var(--line2);justify-content:start}
.brandrow:last-child{border-bottom:none}
.brandrow .bn{color:var(--ink2);font-weight:600;display:flex;align-items:center;gap:var(--sp-1);min-width:0}
.brandrow .cf{color:var(--ink3);font-size:var(--fs-xs);text-align:right}
.brandrow.clik:hover{background:var(--row-hover)}
/* The open brand keeps the SAME accent language as the open category above
 * it — one rail, one wash — so the eye follows the same rule two levels
 * down rather than learning a second one. The rail is an inset shadow for
 * the reason the category's is: .brandrow is a sized grid and a real border
 * would shift every label 3px sideways on open. */
.brandrow.open{background:var(--accent-soft);box-shadow:inset 3px 0 0 var(--accent);border-bottom-color:var(--accent-line)}
.brandrow.open .bn{color:var(--accent)}

/* ===== STORE DRILL-DOWN: THE PRODUCT LEVEL
 *
 * The fourth and last level (owner, 15 Sep 2026). It is a LEAF and is drawn
 * as one: no bar and no percentage, because one fact_reconciliation row can
 * only ever be 0% or 100% and the action pill beside it says the same thing
 * in the app's own words.
 *
 * It sits INSIDE .brandwrap, indented past the brand rows rather than given
 * a second panel of its own. A panel-inside-a-panel at this depth reads as
 * a third thing to understand; an indent reads as "further in", which is
 * all it is. The ground is --depth-2, the second step of the shared depth
 * scale (defined further down this file, with the reasoning for the whole
 * ramp) — the same token /coverage nests with, so the two screens step in
 * alike and all three themes follow without a rule of their own. */
.prodwrap{background:var(--depth-2);border-top:1px solid var(--line2);padding:var(--sp-1) 0}
/* A brand opened before its products have arrived. loadProducts() has
 * always set aria-busy while it fetches; nothing drew it, so the panel was
 * blank until the rows landed (24 Sep 2026). The loader removes aria-busy
 * in its finally, so the line goes the moment the rows (or the error) land. */
.prodwrap[aria-busy="true"]::before{
  content:"Loading products…";display:block;
  padding:var(--sp-2) var(--sp-4);font-size:var(--fs-xs);color:var(--ink3);
}
/* The column headings, which are also the sort buttons. Laid out on the
 * same three-column grid as the rows below so each heading sits over its
 * own column. */
.prodhead{
  display:grid;grid-template-columns:minmax(0,1fr) 250px 76px 82px 76px 76px 140px;align-items:center;
  gap:var(--sp-3);padding:0 var(--sp-3) 0 var(--sp-10);margin-bottom:2px;
  border-bottom:1px solid var(--accent-line);
}
/* The name heading is column 1 and the status heading the LAST one — the
 * code column has no heading of its own, because sorting by code inside one
 * brand sorts by a shared prefix. Placed explicitly so the two buttons land
 * over the columns they order rather than side by side in the first one;
 * `order` puts the name button back in front of the two stock headings,
 * which follow it in the markup. */
.prodhead .prodsort:first-child{grid-column:7}
.prodhead .prodsort:nth-child(2){grid-column:1;order:-1}
/* The two stock headings. Right-aligned over their own right-aligned
 * figures, and not sortable: the list sorts by status or by name, and a
 * third key on a list this short is a control nobody reaches for. */
.prodhead-num{
  grid-row:1;text-align:right;color:var(--ink3);
  font-size:var(--fs-micro);font-weight:600;letter-spacing:.02em;
}
.prodhead-num:nth-of-type(1){grid-column:3}
.prodhead-num:nth-of-type(2){grid-column:4}
.prodhead-num:nth-of-type(3){grid-column:5}
.prodhead-num:nth-of-type(4){grid-column:6}
/* justify-self:start so each button is the width of its own label. As a
 * stretched grid item the name button filled the whole 1fr column, and its
 * hover and focus states drew a 700px box around two words. */
.prodsort{
  justify-self:start;
  background:none;border:none;font:inherit;font-size:var(--fs-micro);font-weight:600;
  color:var(--ink3);cursor:pointer;padding:var(--sp-1) var(--sp-2);margin:0 -8px;
  border-radius:var(--radius-xs);text-align:left;letter-spacing:.02em;
}
.prodsort:hover{color:var(--ink2);background:var(--row-hover)}
/* The active order, in the same accent the open rows above already use, so
 * "this is the state you are in" reads the same way at every depth. */
.prodsort.on,.prodsort.on:hover{color:var(--accent)}
/* Scaffolding on the default button — it explains what "Status" sorts TO,
 * which is the whole reason this order is the default, and it must not
 * compete with the word being clicked. */
.prodsort-tail{font-weight:400;color:var(--ink3)}
.prodsort.on .prodsort-tail{color:var(--ink3)}
/* A flex column PURELY so `order` has something to act on — see the
 * reasoning in category-drilldown.blade.php. Rows are rendered in status
 * order and stay in it; .by-name lets each row's own --po rank take over,
 * which is a reflow rather than a re-sort and touches no DOM node. */
.prodlist{display:flex;flex-direction:column}
.prodlist.by-name .prodrow{order:var(--po)}
/* The left padding is DELIBERATELY past the brand name above it, not merely
 * past the panel edge. At var(--sp-6) a product started at almost exactly
 * the x the brand name did — the arrow and gap on a .brandrow eat the same
 * ~26px — so four levels of hierarchy rendered as three. sp-10 puts a
 * visible step between them.
 *
 * The CODE column is 250px because these are not short: a real kodebarang
 * runs to "NR-RAM-LO-TE-TCR-DDR5-16X2/60EXPBLK", 35 characters, which was
 * being clipped mid-token at 110px — and a truncated product code with no
 * ellipsis to say so is worse than none, because it still looks like a
 * code. */
.prodrow{
  display:grid;grid-template-columns:minmax(0,1fr) 250px 76px 82px 76px 76px 140px;align-items:center;
  gap:var(--sp-3);padding:2px var(--sp-3) 2px var(--sp-10);
  font-size:var(--fs-micro);
}
/* The two stock figures. Right-aligned and tabular so a column of them can
 * be compared by eye down the list, which is the whole reason they are here
 * — 76px holds "not counted" at this size, which is what a product MPL does
 * not carry prints instead of a number. */
.pqty{
  text-align:right;color:var(--ink2);
  font-variant-numeric:tabular-nums;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis;
}
/* THE TWO BIGSELLER FIGURES ARE BOLD (owner, 16 Sep 2026). Four numbers in a
 * row is a lot to read, and they are two PAIRS — what the listing should say
 * against what it does. Weighting BigSeller's half of each pair gives the eye
 * the two values that describe the live listing, with our own figures beside
 * them as the reference. Weight rather than colour, so the three themes
 * inherit it and the achromatic one loses nothing. */
.pstrong{font-weight:700;color:var(--ink)}
/* One line, always. A spesifikasi runs to 80-odd characters and wrapping
 * them turns a list of 40 products into a page of paragraphs; the full text
 * is on the element's title attribute for anyone who needs it. */
.pn{color:var(--ink2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Tabular so the codes form a column the eye can run down, and .nowrap's
 * reasoning applies: a kodebarang broken after its hyphen reads as two. */
.pcode{color:var(--ink3);font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media(max-width:1180px){
  /* The code gives way first. At this width the panel is one column and the
   * NAME is what a person is reading; a code they cannot finish reading is
   * still enough to recognise a product they already know. The heading row
   * has to move with it or the two sort buttons stop sitting over the
   * columns they sort. */
  .prodrow,.prodhead{grid-template-columns:minmax(0,1fr) 110px 62px 68px 62px 62px 130px}
}
@media(max-width:1000px){
  /* Tighter again where the rail still takes 210px (821-1000px): the fixed
   * tracks above left the NAME column 60-110px on a tablet, and at the low
   * end next to nothing (UI review, 26 Sep 2026). Narrower tracks, gaps and
   * indent give it back ~150-250px; the full name stays on the title. */
  .prodrow,.prodhead{grid-template-columns:minmax(0,1fr) 90px 54px 60px 54px 54px 104px;gap:var(--sp-2)}
  .prodrow{padding-left:var(--sp-5)}
}
/* Bar + percentage. The number is a SIBLING of the track, never inside it —
 * see the comment in components/store/category-drilldown.blade.php for why
 * (short version: a label on the fill is least readable exactly on the
 * worst-performing rows, which are the ones being looked for). */
.catbar-wrap{display:flex;align-items:center;gap:var(--sp-2)}
.catbar{flex:1;height:8px;background:var(--line2);border-radius:4px;overflow:hidden;position:relative}
.catbar.sm{height:6px}
/* No min-width. A 0% category must render NO fill at all — a 2px nub reads
 * as "a little bit done", and 0 of 71 listed is exactly the row a user is
 * hunting for. The grey track already shows the row exists. */
.catbar .fill{height:100%;border-radius:4px}
.catbar .fill.red{background:linear-gradient(90deg,var(--bar-red-a),var(--bar-red-b))}
.catbar .fill.yellow{background:linear-gradient(90deg,var(--bar-yellow-a),var(--bar-yellow-b))}
.catbar .fill.green{background:linear-gradient(90deg,var(--bar-green-a),var(--bar-green-b))}
/* 46px, not 34: room for a finished row's ✓ beside "100%" without the
 * finished rows' bars coming out shorter than their neighbours'. */
.catpct{flex:0 0 46px;text-align:right;font-size:var(--fs-micro);font-weight:700;color:var(--ink2);white-space:nowrap}

/* A FINISHED ROW (owner, 26 Sep 2026: "if it's 100%, i think you can give it
 * a special effect"). Keyed on Rollup::isComplete() — every tracked product
 * done — never on the printed number, which rounds 199/200 up to 100%.
 *
 * Three channels, so it never rests on colour alone: a ✓ beside the figure,
 * the figure in OK ink, and a deeper green fill with a soft glow. A light
 * sweeps across the bar ONCE when the row appears and again under the
 * pointer — never on a loop: twenty bars shimmering at rest would read as a
 * page still loading, which is the opposite of "done". */
.catbar-wrap.is-done .catbar{overflow:visible}
.catbar-wrap.is-done .fill{
  position:relative;overflow:hidden;
  background:linear-gradient(90deg,var(--bar-green-b),var(--ok-tx));
  box-shadow:0 0 6px color-mix(in srgb,var(--ok-dot) 55%,transparent);
}
.catbar-wrap.is-done .fill::after{
  content:"";position:absolute;inset:0;
  background:linear-gradient(100deg,transparent 25%,rgba(255,255,255,.6) 50%,transparent 75%);
  transform:translateX(-100%);animation:done-sheen 1.3s ease-out .2s 1 both;
}
.catrow:hover .catbar-wrap.is-done .fill::after,
.brandrow:hover .catbar-wrap.is-done .fill::after,
tbody tr:hover .catbar-wrap.is-done .fill::after{animation-name:done-sheen-again}
@keyframes done-sheen{from{transform:translateX(-100%)}to{transform:translateX(100%)}}
@keyframes done-sheen-again{from{transform:translateX(-100%)}to{transform:translateX(100%)}}
.catbar-wrap.is-done .catpct{color:var(--ok-tx)}
.done-mark{margin-right:3px}
@media (prefers-reduced-motion:reduce){
  .catbar-wrap.is-done .fill::after{animation:none}
}


.shopwide-lbl{font-size:var(--fs-xs);font-weight:600;color:var(--ink2)}


/* APPLIED RULES -- the read-only report's band headings. Reuses the .rl-*
 * row family wholesale (see rule-row.blade.php); all this adds is the
 * scaffolding that separates one division, and one store, from the next.
 * The store band is indented and lighter than the division band because it
 * is subordinate to it -- a store rule overrides its division, but it is
 * still read underneath it. */
/* A division that sets nothing says so in words -- an empty stretch of
 * panel would read as a rendering failure. */
.ar-none{margin:0;padding:var(--sp-3) var(--sp-5);font-size:var(--fs-xs);color:var(--ink3)}

/* action sub-tabs */
.subtabs{display:flex;gap:var(--sp-1);margin-bottom:var(--sp-4);flex-wrap:wrap}
.subtab{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:var(--sp-2) var(--sp-3);font-size:var(--fs-sm);color:var(--ink2);cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:var(--sp-2);font-weight:600}
.subtab:hover:not(:disabled){border-color:var(--accent);color:var(--accent)}
.subtab.on{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
.subtab .ct{background:var(--line2);color:var(--ink3);border-radius:20px;padding:1px var(--sp-2);font-size:var(--fs-micro);font-weight:600}
.subtab.on .ct{background:var(--on-accent-wash);color:var(--on-accent)}

/* ============================================== FOCUS, one definition
 * Every interactive element gets the SAME visible ring. The old file had
 * exactly two focus rules and both began with `outline:none` — .search
 * replaced the outline with a 1px border-colour change (nearly invisible,
 * and invisible entirely to anyone who cannot distinguish those two
 * greys), and everything else (.filter, .btn, .subtab, .store-pick, the
 * pager, every row action) had no focus style at all.
 *
 * :focus-visible rather than :focus so a mouse user does not see a ring on
 * every click, but a keyboard user always does. */
a:focus-visible,button:focus-visible,input:focus-visible,
select:focus-visible,textarea:focus-visible,summary:focus-visible,[tabindex]:focus-visible{
  outline:none;
  box-shadow:var(--focus-ring);
  border-radius:var(--radius-xs);
  position:relative;z-index:1;
}
.field input:focus-visible,.field select:focus-visible,
.search:focus-visible,.filter:focus-visible,.store-pick:focus-visible,
.addbar input:focus-visible,.addbar select:focus-visible{
  border-color:var(--accent);
}
/* On the navy rail the light-blue ring disappears. Use a light ring
 * instead, so keyboard focus is visible on the sidebar too. */
.rail a:focus-visible,.rail button:focus-visible{
  box-shadow:0 0 0 2px var(--sidebar),0 0 0 4px var(--rail-focus);
}

/* ============================================== LOADING FEEDBACK
 * wire:loading appeared ZERO times in this app before. Every filter,
 * sort, tab and page is a server round trip, and the search inputs are
 * debounced 300-500ms — so a slow query and a broken page looked exactly
 * alike, and the honest response to "did that work?" was to click again.
 *
 * .is-busy dims and blocks the region being replaced; .spinner is for
 * inline use next to a control. aria-busy is set alongside these in the
 * markup so the state is announced, not merely drawn. */
/* NB: no `[wire\:loading]{display:none}` rule here — Livewire toggles those
 * elements with inline styles of its own, and a CSS rule racing it breaks
 * the `.flex` / `.inline-flex` display modifiers. Let Livewire own it. */
.is-busy{opacity:.55;pointer-events:none;transition:opacity .12s ease}
.spinner{
  display:inline-block;width:12px;height:12px;flex-shrink:0;
  border:2px solid var(--accent-line);border-top-color:var(--accent);
  border-radius:50%;animation:spin .6s linear infinite;
  vertical-align:-2px;
}
.spinner.on-accent{border-color:var(--on-accent-wash);border-top-color:var(--on-accent)}
@keyframes spin{to{transform:rotate(360deg)}}
.loading-note{display:inline-flex;align-items:center;gap:var(--sp-2);font-size:var(--fs-xs);color:var(--ink3)}

/* Screen-reader-only, for live regions and labels that must not show.
 *
 * text-transform:none is not cosmetic here — .sr-only text sits inside
 * elements that ARE transformed (the table headers are uppercased), and
 * some screen readers spell an all-caps string out letter by letter. The
 * sentence "sorted ascending, activate to sort descending" must not be
 * announced as "S-O-R-T-E-D". Same reason letter-spacing is reset. */
.sr-only{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;
  text-transform:none;letter-spacing:normal;
}

/* ---------- alerts / empty states ---------- */
.alert{border-radius:8px;padding:10px var(--sp-3);font-size:var(--fs-sm);margin-bottom:var(--sp-3)}
.alert-error{background:var(--urgent-bg);color:var(--urgent-tx);border:1px solid var(--urgent-line)}
.alert-ok{background:var(--ok-bg);color:var(--ok-tx);border:1px solid var(--ok-line)}
/* "Waiting" is not "wrong". A pending request is normal work in progress, so
 * it gets its own calm tone rather than borrowing the error red (which would
 * teach people to ignore red) or the success green (which would say the
 * thing has happened, when the entire point is that it has not). */
.alert-pending{background:var(--input-bg);color:var(--input-tx);border:1px solid var(--input-line)}
.alert-pending a{color:inherit;font-weight:700}
/* ===== THE OUTCOME OF A PRESS, WHERE THE READER IS LOOKING (24 Sep 2026)
 *
 * On a dozen screens a button's result was drawn at the TOP of the page
 * while the button sat on a row or a bottom bar, often a screen or more
 * away — the owner's "when i click save, there's no feedback" from
 * /management, on every screen /management's own fix never reached. A
 * sticky message cannot work here (.content scrolls — see the layout's note
 * on the scope strip), so these float in the window's corner instead:
 * position:fixed ignores every scrolling ancestor. Dismiss clears the
 * component's own message property, so the next outcome always shows.
 * A bottom bar, when one is up, keeps its place and the toast sits above it. */
.alert.toast{
  position:fixed;right:var(--sp-5);bottom:var(--sp-5);z-index:80;
  max-width:min(30rem,calc(100vw - 2 * var(--sp-5)));margin:0;
  padding-right:var(--sp-8);
  box-shadow:0 8px 24px rgba(var(--shadow-rgb),.18);
}
body:has(.bulkbar) .alert.toast{bottom:calc(var(--sp-5) + 64px)}
.toast-close{
  position:absolute;top:var(--sp-1);right:var(--sp-2);
  background:none;border:0;padding:0 var(--sp-1);font:inherit;font-size:var(--fs-md);line-height:1;
  color:inherit;opacity:.7;cursor:pointer;
}
.toast-close:hover{opacity:1}
@media print{ .alert.toast{display:none} }

/* Nav count for pending approvals. Only rendered when non-zero and only for
 * people who can act on it — see AppServiceProvider's composer. */
.nav-pill{
  margin-left:auto;background:var(--accent);color:var(--on-accent);
  border-radius:20px;padding:1px var(--sp-2);
  font-size:var(--fs-micro);font-weight:700;
}
.nav a.on .nav-pill{background:rgba(255,255,255,.24)}

/* ---------- approval queue ---------- */
/* The consequence, in words, ABOVE the code — an approver decides on what it
 * does, not on a SKU they have to decode first. */

/* ---------- category rules board ---------- */
/* The two answer columns need sub-labels: "Counts as" and "Listing" are
 * short enough to look interchangeable, and the difference between them —
 * one moves the pile, the other moves the denominator — is the single thing
 * most likely to be got wrong once an editor exists. */
.empty{padding:var(--sp-8) var(--sp-5);text-align:center;color:var(--ink3);font-size:var(--fs-sm)}
.empty .btn-sm{margin-top:var(--sp-3)}

/* Movement on the division dashboard.
 *
 * Green up / red down is the one place in this app where colour alone would
 * carry meaning, so it never does: every delta ships with an arrow (▲ ▼) or
 * the words "no change" / "no data". Red-green is also the commonest colour
 * blindness, and this screen is a ranking — the two ends of it must not be
 * the two colours hardest to tell apart, unaided. */
.delta{font-weight:700;font-size:var(--fs-xs);white-space:nowrap}
.delta.up{color:var(--ok-tx)}
.delta.down{color:var(--stock-tx)}
.delta.flat{color:var(--ink3);font-weight:600}
.delta-note{font-size:var(--fs-micro);color:var(--ink3);font-weight:600;margin-left:var(--sp-2);white-space:nowrap;cursor:help}

/* THE MENU BUTTON AND THE RAIL DRAWER (24 Sep 2026) — see the layout's
 * comment on .shell. Hidden above 820px, where the rail is always there. */
.nav-toggle{
  display:none;align-items:center;gap:var(--sp-2);
  margin-bottom:var(--sp-2);padding:var(--sp-1) var(--sp-3);
  border:1px solid var(--line);border-radius:var(--radius-sm);
  background:var(--control-bg);color:var(--ink);font:inherit;font-size:var(--fs-sm);font-weight:600;cursor:pointer;
}
.nav-toggle .ico{width:16px;height:16px}
.rail-scrim{position:fixed;inset:0;z-index:59;background:rgba(var(--shadow-rgb),.45)}
@media(min-width:821px){
  /* Widening the window with the drawer open must not leave a scrim over
   * a page whose rail is back in its column. */
  .rail-scrim{display:none !important}
}

@media(max-width:820px){
  .rail{display:none}
  .nav-toggle{display:inline-flex}
  .rail.is-open{
    display:flex;position:fixed;top:0;left:0;bottom:0;z-index:60;
    height:100vh;overflow:auto;box-shadow:0 0 40px rgba(var(--shadow-rgb),.35);
  }
  .cards,.cards.cols-3{grid-template-columns:repeat(2,1fr)}
  .acttiles{grid-template-columns:repeat(2,1fr)}
  .hero-figures{gap:var(--sp-4)}
  .store-summary{grid-template-columns:1fr}
  .catgrid{grid-template-columns:1fr}
}

/* Respect the OS setting. The only motion in this app is the spinner and
 * the skip link; both become instant rather than disappearing, so the
 * busy state is still visible to someone who has asked for no animation. */
@media(prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.01ms !important;animation-iteration-count:1 !important;
    transition-duration:.01ms !important;
  }
  .spinner{border-top-color:var(--accent);opacity:.9}
}

/* ---------- category rule editor ---------- */
/*
 * The drawer where a rule is composed and measured. It sits INSIDE the
 * board's panel rather than floating over it, so the tree the person was
 * reading stays visible underneath — a modal here would hide the very thing
 * the rule is about.
 *
 * The two questions are separated by a rule and given real legends, not
 * placed side by side. Side by side reads as "pick one of these six things";
 * stacked with a divider reads as two decisions, which is what they are.
 */
.rf-slice{font-size:var(--fs-sm);color:var(--ink2)}
.rf-close{margin-left:auto}

.rf-q{border:none;padding:0;margin:0 0 var(--sp-4)}
.rf-q legend{font-size:var(--fs-sm);font-weight:650;color:var(--ink);padding:0;margin-bottom:var(--sp-2)}
.rf-q + .rf-q{border-top:1px solid var(--line2);padding-top:var(--sp-4)}

.rf-choices{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:var(--sp-3)}

/* A card, not a radio in a row. The consequence sentence is the thing being
 * chosen between, so it has to be inside the target you click. */
.rf-choice{
  display:block;position:relative;background:var(--panel);border:1px solid var(--line);
  border-radius:var(--radius);padding:var(--sp-3) var(--sp-3) var(--sp-3) var(--sp-8);
  cursor:pointer;font-size:var(--fs-sm);
}
.rf-choice:hover{border-color:var(--accent-line)}
.rf-choice.on{border-color:var(--accent);background:var(--accent-soft)}
.rf-choice input{position:absolute;left:var(--sp-3);top:var(--sp-4)}
.rf-choice-t{display:block;font-weight:650;color:var(--ink);margin-bottom:var(--sp-1)}
.rf-choice-d{display:block;color:var(--ink2);line-height:1.5}
.rf-choice-d .pill{vertical-align:baseline}

/* The listing question, present but not yet available. Greyed, and never
 * clickable — an affordance that does nothing is worse than none. */
.rf-note{font-size:var(--fs-xs);color:var(--ink3);margin-top:var(--sp-2);line-height:1.6}
.rf-note a{color:var(--accent);font-weight:600}

/* The product drawer lays its fields out the same way (UI review, 27 Sep
 * 2026: .rdrawer-row had no rule at all, so its scope selects and note box
 * rendered as bare browser controls). */
.rf-row,.rdrawer-row{display:flex;gap:var(--sp-3);flex-wrap:wrap;align-items:end;margin-bottom:var(--sp-4)}
.rdrawer-body > .fld{margin-bottom:var(--sp-4)}
.rf-row .fld,.rdrawer-body .fld{display:flex;flex-direction:column;gap:var(--sp-1)}
.rf-row .fld label,.rdrawer-body .fld label{font-size:var(--fs-micro);color:var(--ink3);font-weight:600}
.rf-row input,.rf-row select,.rdrawer-body .fld input,.rdrawer-body .fld select{
  padding:7px 10px;border:1px solid var(--line);border-radius:var(--radius-sm);
  font-size:var(--fs-sm);font-family:inherit;color:var(--ink);background:var(--control-bg);
}
.rf-row .rf-grow,.rdrawer-row .rf-grow{flex:1 1 240px}
.rf-row .rf-grow input,.rdrawer-row .rf-grow input,.rdrawer-body > .fld input{width:100%}
.rf-opt{font-weight:500;text-transform:none;letter-spacing:0}

.rf-actions{display:flex;align-items:center;gap:var(--sp-3);flex-wrap:wrap}
.rf-withdraw{margin-left:auto;color:var(--stock-tx)}
.rf-recheck{font-size:var(--fs-micro);color:var(--ink3);flex:1 1 200px}

/* ---------- the impact step ---------- */
.rf-impact{border-top:1px solid var(--line);margin-top:var(--sp-4);padding-top:var(--sp-4)}

/* The number is the largest thing on the screen at this moment, because it
 * is the only thing that must not be skimmed past. */
.rf-imp-head{display:flex;align-items:baseline;gap:var(--sp-3);margin-bottom:var(--sp-3);flex-wrap:wrap}
.rf-imp-n{font-size:var(--fs-hero);font-weight:700;color:var(--ink);line-height:1}
.rf-imp-l{font-size:var(--fs-base);color:var(--ink2)}
.rf-imp-none{display:block;font-size:var(--fs-sm);color:var(--ink2);line-height:1.6}

.rf-imp-table{width:100%;border-collapse:collapse;margin-bottom:var(--sp-4);background:var(--panel);
  border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}
.rf-imp-table th{
  text-align:left;font-size:var(--fs-micro);text-transform:uppercase;letter-spacing:.05em;
  color:var(--ink3);font-weight:700;padding:var(--sp-2) var(--sp-3);border-bottom:1px solid var(--line);
}
.rf-imp-table td{padding:var(--sp-2) var(--sp-3);font-size:var(--fs-sm);border-bottom:1px solid var(--line2)}
.rf-imp-table tr:last-child td{border-bottom:none}
.rf-imp-table .num{text-align:right;font-variant-numeric:tabular-nums}
.rf-imp-table .delta{display:block;font-size:var(--fs-micro);margin-top:2px}

/* Green, and stated as strongly as any warning. Reassurance that is
 * whispered gets read as hedging. */
.rf-imp-safe{
  display:flex;gap:10px;align-items:flex-start;background:var(--ok-bg);color:var(--ok-tx);
  border:1px solid var(--ok-line);border-radius:var(--radius);padding:var(--sp-3) var(--sp-4);
  font-size:var(--fs-sm);line-height:1.55;margin-bottom:var(--sp-4);
}
.rf-imp-safe .ic{font-weight:800}

.rf-imp-not{
  background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);
  padding:var(--sp-3) var(--sp-4);margin-bottom:var(--sp-4);
}
.rf-imp-not h4{font-size:var(--fs-sm);font-weight:650;margin-bottom:var(--sp-2)}
.rf-imp-not ul{margin:0;padding-left:var(--sp-4)}
.rf-imp-not li{font-size:var(--fs-sm);color:var(--ink2);line-height:1.6;margin-bottom:var(--sp-1)}

/* The typed confirmation. Deliberately not styled as a small aside — it is
 * the last thing between an unreviewed write and thousands of rows. */
.rf-confirm{
  background:var(--urgent-bg);border:1px solid var(--urgent-line);border-radius:var(--radius);
  padding:var(--sp-3) var(--sp-4);margin-bottom:var(--sp-4);
  display:flex;align-items:center;gap:var(--sp-3);flex-wrap:wrap;
}
.rf-confirm label{font-size:var(--fs-sm);color:var(--urgent-tx)}
.rf-confirm-in{
  width:110px;padding:7px 10px;border:1px solid var(--urgent-line-strong);border-radius:var(--radius-sm);
  font-size:var(--fs-base);font-family:inherit;text-align:center;
  font-variant-numeric:tabular-nums;background:var(--control-bg);color:var(--ink);
}

/* The radio inside a card is small; the global focus ring lands on it and
 * is easy to miss. Lift it to the whole card, which is what is actually
 * being chosen. */
.rf-choice:has(input:focus-visible){border-color:var(--accent);box-shadow:var(--focus-ring)}

@media(max-width:820px){
  .rf-choices{grid-template-columns:1fr}
  .rf-imp-table{display:block;overflow-x:auto}
}

/* Settings attribution on the division dashboard.
 *
 * Deliberately NOT red. A rule change is not a fault — it is context the
 * reader needs before crediting or blaming anyone, and colouring it as a
 * warning would teach people that supervisors doing their job is a problem. */
.delta-rule{
  font-size:var(--fs-micro);color:var(--ink3);font-weight:600;margin-left:var(--sp-2);
  white-space:nowrap;cursor:help;
}

/* ---------- rule editor: the two questions ---------- */
/*
 * A segmented switch, not a dropdown. The two questions have different
 * consequences — one moves work between piles, the other changes what a
 * store is scored out of — so both are visible at all times and picking one
 * is a deliberate act. A <select> would hide the other option behind a
 * click and make them feel interchangeable.
 */
.rf-switch{display:flex;gap:0;margin-bottom:var(--sp-4);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;width:fit-content;max-width:100%;flex-wrap:wrap}
.rf-tab{
  background:var(--control-bg);border:none;padding:var(--sp-2) var(--sp-4);cursor:pointer;
  font-family:inherit;font-size:var(--fs-sm);font-weight:600;color:var(--ink2);
}
.rf-tab + .rf-tab{border-left:1px solid var(--line)}
.rf-tab:hover{background:var(--accent-soft)}
.rf-tab.on{background:var(--accent);color:var(--on-accent)}

/* The carve-out. A modifier of the answer above, indented under it, never a
 * fourth choice in the row — "not our job, except this one" is one decision
 * with an exception, not two decisions. */
.rf-exempt{
  display:flex;gap:10px;align-items:flex-start;margin-top:var(--sp-3);
  padding:var(--sp-3);border:1px dashed var(--line);border-radius:var(--radius);
  font-size:var(--fs-sm);color:var(--ink2);line-height:1.55;cursor:pointer;
}
.rf-exempt input{margin-top:3px;flex:0 0 auto}
.rf-exempt:hover{border-color:var(--accent-line)}

/* The denominator warning.
 *
 * Red, and the same red as a refusal, because this is the one change that
 * moves a store's score without anyone doing any work — and when it shrinks
 * the denominator it moves it in the direction nobody investigates. It is
 * not styled as an error the person caused; it is styled as something they
 * must not skim. */
.rf-imp-warn{
  display:flex;gap:10px;align-items:flex-start;background:var(--urgent-bg);color:var(--urgent-tx);
  border:1px solid var(--urgent-line);border-radius:var(--radius);padding:var(--sp-3) var(--sp-4);
  font-size:var(--fs-sm);line-height:1.55;margin-bottom:var(--sp-4);
}
.rf-imp-warn .ic{font-weight:800}

/* Five columns of before/after do not fit a narrow window; the table scrolls
 * inside its own box rather than the page scrolling sideways. */
.rf-imp-scroll{overflow-x:auto;margin-bottom:var(--sp-4)}
.rf-imp-scroll .rf-imp-table{margin-bottom:0}

/* ---------- change log ----------
 * The screen that makes rule_change_log's "detective, not preventive"
 * justification true rather than aspirational (see App\Livewire\Rules\
 * ChangeLog's docblock). One card per applied change, newest first — the
 * same card shape as .req (the approval queue) but its own class names,
 * because this list is never decided on and needs no checkbox or action
 * row, only a head, a body, and an optional disclosure. */
/* Never merged with the actor's name — actor is who pressed the button,
 * on_behalf_of is who asked, and the two must stay visibly separate or an
 * approver is credited with someone else's decision. */
.cl-onbehalf{display:block;font-size:var(--fs-xs);color:var(--ink3)}
.cl-meta{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap;font-size:var(--fs-xs);color:var(--ink3);margin-top:2px}
/* Same red as a refusal (--urgent-*), and for the same reason .rf-imp-warn
 * uses it: this is the one flag on the page that must not be skimmed past,
 * not a decoration. */
.cl-high{display:inline-block;padding:2px var(--sp-2);border-radius:var(--radius-xs);font-size:var(--fs-micro);font-weight:700;background:var(--urgent-bg);color:var(--urgent-tx)}
/* The same shape in neutral ink: a narrow change that moved the completion
 * denominator — worth knowing, not an alarm. */
.cl-note{display:inline-block;padding:2px var(--sp-2);border-radius:var(--radius-xs);font-size:var(--fs-micro);font-weight:700;border:1px solid var(--line);color:var(--ink2)}

/* The impact snapshot, collapsed by default so a page of quiet one-product
 * changes stays scannable. A native <details> rather than a Livewire
 * toggle: nothing here is fetched — the JSON is already on the page — so a
 * round trip would buy nothing, and the browser supplies the expand/collapse
 * semantics and keyboard handling for free. */
.cl-impact{margin-top:var(--sp-2)}
.cl-impact summary{cursor:pointer;font-size:var(--fs-xs);font-weight:650;color:var(--accent);width:fit-content}
.cl-impact summary:hover{text-decoration:underline}
.cl-impact[open] summary{margin-bottom:var(--sp-2)}
.cl-impact-note{font-size:var(--fs-xs);color:var(--ink3);margin-bottom:var(--sp-2)}
.cl-impact-warn{color:var(--urgent-tx);font-weight:600}

/* ===================================================== LISTING RULES
 * The merged "Control lists" + "Category rules" screen (see the plan:
 * C:\Users\E0206\.claude\plans\please-go-and-read-fuzzy-puppy.md). Two
 * pages — Rules (a list of every rule) and Catalogue (the taxonomy tree,
 * with per-product entries rolled into its finest level) — sharing one
 * drawer and one bulk-selection bar. Every class below is frozen by that
 * plan; five other agents write markup against these exact names, so they
 * are defined here even though nothing consumes them yet.
 */

/* ---------- sidebar: Listing rules' own sub-menu ----------
 * Same spine pattern as Reconciliation's .subnav (reused unchanged) — this
 * class only carries what's DIFFERENT about it. Two destinations instead of
 * seven, and no coloured .dot: the seven reconciliation tabs are an ordered
 * waterfall, and the dot's hue is part of that story; Rules and Catalogue
 * are two unordered views of the same data, so a dot would imply a
 * sequence that is not real. */
.rules-subnav{margin-bottom:var(--sp-2)}

/* ---------- the drawer ----------
 * position:fixed, right-hand, full height, above the page, with a scrim.
 * This is the fix for the editor's worst bug: today's editor renders
 * INLINE, above a 121-row table, so opening it from row 90 puts the form
 * off-screen and shoves the whole page down — and saving changes the page
 * height twice more, so you never land back where you were. Because the
 * drawer is `position:fixed` it occupies its own layer, not a row in the
 * document, so the page underneath CANNOT reflow — the bug is fixed by
 * construction, not by careful scroll-restoration code.
 */
.rdrawer-scrim{
  position:fixed;inset:0;z-index:70;
  background:rgba(10,16,30,.44);
  opacity:0;visibility:hidden;cursor:default;
  transition:opacity .16s ease,visibility 0s linear .16s;
}
.rdrawer-scrim.on{opacity:1;visibility:visible;transition:opacity .16s ease}
/* Deliberately NO :hover/:active affordance and no click-to-close wired
 * here — that is a markup/JS decision (the drawer holds typed work, and a
 * stray click on the scrim must never be how it is lost; Esc and the
 * explicit Cancel button are the only doors out), but the cursor stays
 * `default` rather than `pointer` so the scrim does not even LOOK
 * clickable. */

.rdrawer{
  position:fixed;top:0;right:0;bottom:0;z-index:71;
  width:480px;max-width:92vw;
  background:var(--panel);
  display:flex;flex-direction:column;
  transform:translateX(100%);
  transition:transform .18s ease,box-shadow .18s ease;
}
/* The shadow only while OPEN: parked off-screen at translateX(100%), a
 * -12px 40px shadow still bled a pale band down the right edge of every page
 * that mounts the drawer (page audit, 26 Sep 2026). */
.rdrawer.on{transform:translateX(0);box-shadow:-12px 0 40px rgba(var(--shadow-rgb),.24)}
/* The dialog inside the drawer (24 Sep 2026). Both drawers render their
 * dialog in an inner element that exists only while open; it has to carry
 * the drawer's column layout, or the body stops being the part that
 * scrolls and a long form pushes its buttons out of reach. */
.rdrawer-panel{display:flex;flex-direction:column;flex:1 1 auto;min-height:0}
.rdrawer-head{
  flex:0 0 auto;display:flex;align-items:flex-start;gap:var(--sp-3);
  padding:var(--sp-4) var(--sp-5);border-bottom:1px solid var(--line);
}
.rdrawer-head h2,.rdrawer-head h3{font-size:var(--fs-md);font-weight:650;line-height:1.35}
/* The slice/target stays on screen with the rest of the head no matter how
 * long the body scrolls — this is bug #2's other half: an edit that names
 * the wrong level (GROUP instead of CATEGORY) is only safe to make if what
 * you are about to change stays legible the whole time you are looking at
 * the form, not just when the drawer first opens. */
.rdrawer-slice{font-size:var(--fs-sm);color:var(--ink2);margin-top:2px}
/* flex:1 1 auto + its own overflow, NOT the drawer's: the head and foot
 * must stay pinned while only the middle scrolls — the same "content vs.
 * content-alert" flex:1 trap documented in layouts/app.blade.php, avoided
 * here by giving exactly one child flex:1. */
.rdrawer-body{flex:1 1 auto;overflow-y:auto;padding:var(--sp-5)}
.rdrawer-foot{
  flex:0 0 auto;display:flex;align-items:center;gap:var(--sp-3);flex-wrap:wrap;
  padding:var(--sp-4) var(--sp-5);border-top:1px solid var(--line);
  background:var(--inset);
}
.rdrawer-foot .rf-withdraw{margin-left:auto}
/* A footer written INSIDE the scrolling body (the product drawer has one per
 * mode) sticks to the body's bottom edge, so a long form never scrolls its
 * button out of reach. The negative margins let it run edge to edge. */
.rdrawer-body > .rdrawer-foot{
  position:sticky;bottom:calc(-1 * var(--sp-5));
  margin:var(--sp-5) calc(-1 * var(--sp-5)) calc(-1 * var(--sp-5));
}

@media(max-width:820px){
  .rdrawer{width:100vw;max-width:100vw}
}

/* ---------- Page 1: the rules list ----------
 * One card per rule — same family as .req (approval queue) and .cl-row
 * (change log): a head line of short fixed-width facts, an optional note,
 * and a row of actions. Kept a DISTINCT class family (.rl-*) rather than
 * reusing .req/.cl-row because a rule row carries a fifth thing neither of
 * those does — a live warning flag (.rl-flag) — and forcing it into either
 * existing shape would mean overloading a class that other screens already
 * give a fixed meaning.
 */
.rl-row{border-bottom:1px solid var(--line2);padding:var(--sp-2) var(--sp-5)}
.rl-row:last-of-type{border-bottom:none}
.rl-row:hover{background:var(--row-hover)}
/* The five head facts, in the order the mockup shows them: SAYS · SLICE ·
 * APPLIES TO (scope) · REACH · SET BY (who). Flex with fixed-ish bases
 * rather than a grid so the row degrades gracefully — wraps to a second
 * line on a narrow window instead of squeezing every column unreadably
 * thin, same tradeoff .req already makes. */
.rl-says{flex:0 0 132px;font-weight:650;color:var(--ink);font-size:var(--fs-sm)}
.rl-slice{flex:1 1 260px;min-width:0;color:var(--ink2);font-size:var(--fs-sm)}
.rl-scope{flex:0 0 190px;color:var(--ink2);font-size:var(--fs-sm)}
/* Wide enough for "20 products · 9 stores" on ONE line. At 64px it wrapped to
 * two or three, and since .rl-head sizes to its tallest cell that made a
 * one-line rule stand three lines tall. nowrap keeps it honest if the copy
 * ever grows: it will overflow visibly rather than silently reflow the row. */
.rl-reach{flex:0 0 155px;text-align:right;white-space:nowrap;color:var(--ink2);font-size:var(--fs-sm);font-variant-numeric:tabular-nums}
.rl-who{flex:0 0 100px;text-align:right;color:var(--ink3);font-size:var(--fs-xs);white-space:nowrap}
/* The five above sit together as one flex line, and .rl-head is that line.
 * It used to be described here as markup the caller should supply inline —
 * "the plan freezes the leaf class names, not a wrapper name" — and no
 * caller ever did, so every cell stayed a block and one rule stood five
 * lines tall. Given a name here instead: the leaf names are still the
 * frozen ones, and the wrapper now exists rather than being a suggestion. */
.rl-head{display:flex;gap:var(--sp-3);flex-wrap:wrap;align-items:baseline}
/* What the setting MEANS, beside the slice it describes. Muted and small: a
 * reminder for the reader who does not live in this vocabulary, not a second
 * fact competing with the pill. Inline, so it costs no extra line. */
.rl-gloss{font-size:var(--fs-xs);color:var(--ink3)}
.rl-note{font-size:var(--fs-sm);color:var(--ink2);font-style:italic;margin-top:var(--sp-2);line-height:1.55}
/* Inside .rl-head now rather than a block beneath it. margin-left:auto pins
 * it to the end of the line whatever the cells before it measure. */
.rl-act{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap;margin-left:auto}

/* The two "this rule is not what it looks like" flags — both invisible
 * anywhere in the app today and both easy to mistake for "waiting for
 * tonight's run" (see the plan). A shared base plus two tone modifiers,
 * same pattern as the action pills below. */
.rl-flag{display:flex;gap:10px;align-items:flex-start;font-size:var(--fs-xs);line-height:1.55;margin-top:var(--sp-2);padding:var(--sp-2) var(--sp-3);border-radius:var(--radius-sm);border:1px solid transparent}
.rl-flag .ic{font-weight:800;flex-shrink:0}
/* MATCHES NOTHING — the slice this rule targets is empty (bug #3's own
 * detector, per the plan). Amber, not red: the rule was not necessarily
 * wrong when it was written, the catalogue moved under it, so this reads
 * as "worth a look" rather than "you broke something". Same construction
 * as --price-*, one step down in urgency from a refusal. */
.rl-flag-dead{background:var(--price-bg);color:var(--price-tx);border-color:var(--price-line)}
/* PARTLY/FULLY SHADOWED — a narrower, later rule already wins some or all
 * of this rule's slice. Blue/informational (--input-*): this is not a
 * fault, it is how the arbiter is SUPPOSED to work (EffectiveRules'
 * narrowest-wins order) — the flag exists so a person can see it, not so
 * they feel warned. */
.rl-flag-shadow{background:var(--input-bg);color:var(--input-tx);border-color:var(--input-line)}

/* ---------- Page 2: the catalogue tree ----------
 * Replaces the old 121-row flat <table> with a collapsible tree: Section/
 * Group (collapsed to one row wherever they are 1:1 — nine times in ten)
 * down through Category, Brand, and finally individual products. Built as
 * stacked flex rows rather than a <table> on purpose — a table's column
 * count cannot change between an ordinary row and a brand sub-panel or a
 * product leaf without empty cells or colspan gymnastics, and every row
 * here already needs `wire:key` and independent wrapping regardless.
 */


      


.cat-select{flex:0 0 auto;width:16px;height:16px;accent-color:var(--accent);cursor:pointer}
/* A read-only reader gets no checkbox, but the COLUMN still has to be
 * there: .cat-crow and .cat-brow are grids with a fixed 16px first (or
 * second) track, so dropping the box outright slid every later cell one
 * column left and squeezed the name into 16px. Staff are the largest
 * audience for the stores board and the only one that never sees a
 * checkbox, so this is the layout most readers of that screen get. */
.cat-select.ph{visibility:hidden;cursor:default}


/* ---------- the bulk bar ----------
 * A sticky selection footer, not a modal: selecting a slice must not
 * interrupt browsing the rest of the tree. Sticks to the bottom of its own
 * scrolling container (the panel/content well), not the viewport — it is a
 * property of the catalogue list, not a global app chrome element, so it
 * scrolls out with the panel rather than floating over unrelated pages.
 */
.bulkbar{
  position:sticky;bottom:0;z-index:5;
  display:flex;align-items:center;gap:var(--sp-3);flex-wrap:wrap;
  background:var(--panel);border-top:1px solid var(--line);
  padding:var(--sp-3) var(--sp-5);
  box-shadow:0 -6px 16px rgba(var(--shadow-rgb),.08);
}
.bulkbar-n{font-size:var(--fs-sm);font-weight:650;color:var(--ink)}
.bulkbar-act{margin-left:auto;display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap}
/*
 * FIXED, NOT STICKY — and that is a fact about this layout, not a
 * preference.
 *
 * Sticky resolves against the nearest ancestor whose overflow is not
 * visible. Here that is .content (overflow:auto), and nothing constrains
 * its height, so the WINDOW scrolls and .content never does: a scroll
 * container with zero range, which leaves a sticky child pinned to
 * nothing. Two attempts died on it — inside .panel (overflow:hidden), then
 * below the panel as a sticky footer with no containing block left
 * underneath. Measured in the page rather than guessed:
 * .content.scrollHeight === .content.clientHeight.
 *
 * Offset by the rail's 210px so it does not lie across the navy nav, and
 * square-edged because it now spans an edge of the window rather than
 * floating inside a card. Below 820px the rail is display:none, so the
 * offset goes with it.
 */
.bulkbar.standalone{
  position:fixed;left:210px;right:0;bottom:0;
  border-radius:0;border:none;border-top:1px solid var(--line);
  box-shadow:0 -6px 16px rgba(var(--shadow-rgb),.10);
}
/* Reserves the bar's own height at the end of the page so the last row is
 * never hidden behind it. Rendered only while the bar is. */
.bulkbar-spacer{height:60px}
@media(max-width:820px){
  .bulkbar.standalone{left:0}
}

/* ---------- division picker (Listing rules -> Divisions) ---------- */
/*
 * Buttons, not a <select>. There are four divisions and the whole screen is
 * about comparing them — a dropdown hides three of the four behind a click
 * and makes "which am I looking at" a thing you have to remember rather than
 * see. Same reasoning as the worklist's .subtabs.
 */
.div-picker{display:flex;gap:var(--sp-2);flex-wrap:wrap;align-items:center}
/* The Applied-rules store picker, beside the division pills: it narrows the
 * same view rather than asking a second question, so it shares their row. */
.ar-storepick{display:flex;align-items:center;flex-wrap:wrap;gap:var(--sp-2) var(--sp-3);flex-basis:100%}
.ar-storepick-lbl{font-size:var(--fs-xs);font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--ink2)}
.ar-switch{position:relative}
.ar-switch-btn{display:inline-flex;align-items:center;gap:var(--sp-3);min-width:min(280px,100%);justify-content:space-between;
  padding:var(--sp-2) var(--sp-3);border:2px solid var(--accent-line);border-radius:var(--radius-sm);
  background:var(--control-bg);color:var(--ink);font:inherit;font-size:var(--fs-md);font-weight:600;cursor:pointer}
.ar-switch-btn:focus-visible{box-shadow:var(--focus-ring)}
.ar-storepick.is-on .ar-switch-btn{background:var(--accent);color:var(--on-accent);border-color:var(--accent)}
.ar-switch-caret{font-size:var(--fs-xs);opacity:.8}
/* The popover: a search box, then Recent and All shops (Postman's workspace
 * switcher). Absolute inside .ar-switch, which carries no transform. */
.ar-switch-pop{position:absolute;z-index:40;top:calc(100% + 6px);left:0;width:max(100%,300px);
  background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:var(--sp-2);
  box-shadow:0 12px 32px rgba(var(--shadow-rgb),.18)}
.ar-switch-q{width:100%;margin-bottom:var(--sp-2)}
.ar-switch-list{list-style:none;margin:0;padding:0;max-height:min(60vh,420px);overflow:auto}
.ar-switch-list [role=option]{display:flex;align-items:center;gap:var(--sp-2);padding:6px var(--sp-2);border-radius:8px;cursor:pointer;color:var(--ink);font-weight:500}
.ar-switch-list [role=option]:hover,.ar-switch-list [role=option].is-active{background:var(--row-hover)}
.ar-switch-list [role=option][aria-selected=true]{font-weight:700;color:var(--accent)}
.ar-switch-tick{width:1em;flex:none;color:var(--accent)}
.ar-switch-head{padding:var(--sp-2) var(--sp-2) 2px;font-size:var(--fs-micro);font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--ink3)}
.ar-switch-empty{padding:var(--sp-2);color:var(--ink3);font-size:var(--fs-sm)}
/* "Blacklist for" — the words that turn the bulk bar's controls into one
 * sentence rather than a row of unrelated inputs. */
.bulkbar-lbl{font-size:var(--fs-xs);font-weight:600;color:var(--ink2);white-space:nowrap}
.div-btn{
  background:var(--control-bg);border:1px solid var(--line);border-radius:20px;
  padding:var(--sp-2) var(--sp-4);cursor:pointer;font-family:inherit;
  font-size:var(--fs-sm);font-weight:600;color:var(--ink2);white-space:nowrap;
}
.div-btn:hover:not(.on){background:var(--accent-soft);border-color:var(--accent-line)}
.div-btn.on{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
/* A count inside a pill is `.muted`, which is grey ink: fine on white, and
 * next to invisible on the selected pill's accent fill (UI review, 26 Sep
 * 2026 — "Everything 622" read as "Everything"). On the fill it takes the
 * pill's own ink, lighter in weight so it still reads as a count. */
.div-btn.on .muted{color:inherit;font-weight:400}

/* ---------- tier traffic light (Listing rules -> Divisions, group rows) ---------- */
/*
 * Primary / Secondary / Skip, as one three-way control rather than a pill
 * plus a button. The colours are NOT new: they are the same three the
 * worklist already uses for OK, UPDATE PRICE and UPDATE STOCK, so somebody
 * who has read a worklist this week already knows green means settled,
 * amber means attend to it later, red means it is off the books.
 *
 * The COLOUR IS NEVER THE ONLY SIGNAL. Each segment carries its word, the
 * chosen one is filled and the other two are outlined, and every segment
 * carries aria-pressed — so the state survives a monochrome screen, a
 * colour-blind reader and a screen reader alike.
 *
 * All three stay ENABLED, including the current one. A disabled button
 * leaves the tab order, which would put the one setting actually in force
 * out of a keyboard user's reach while leaving the other two available.
 *
 * Inactive segments keep their own ink on a white ground rather than going
 * grey. Greying them would make the control read as "one setting and two
 * disabled things"; they are three equal choices, one of which is current.
 */
.tier3{display:inline-flex;border-radius:6px;overflow:hidden;border:1px solid var(--line)}
.tier3-btn{
  appearance:none;border:0;border-right:1px solid var(--line);
  padding:3px var(--sp-3);cursor:pointer;font-family:inherit;
  font-size:var(--fs-xs);font-weight:700;letter-spacing:.01em;
  background:var(--control-bg);white-space:nowrap;
}
.tier3-btn:last-child{border-right:0}

/* off state — the word in its own colour, on white */
.tier3-primary{color:var(--ok-tx)}
.tier3-secondary{color:var(--price-tx)}
.tier3-skip{color:var(--stock-tx)}

/* on state — the same colour as the ground it names */
.tier3-btn.on.tier3-primary{background:var(--ok-bg)}
.tier3-btn.on.tier3-secondary{background:var(--price-bg)}
.tier3-btn.on.tier3-skip{background:var(--stock-bg)}
/* *** AND UNMISTAKABLE, NOT JUST TINTED (UI review, 26 Sep 2026). *** The
 * pale ground alone left "which one is on?" a squint on the screen whose
 * whole job is that decision. The chosen segment adds a ring and a tick in
 * its own ink (currentColor, so every theme follows), while the other two
 * keep their word on white as the comment above requires. The tick is
 * decoration: aria-pressed already tells a screen reader, so its alt text is
 * empty. */
.tier3-btn.on{box-shadow:inset 0 0 0 2px currentColor}
.tier3-btn.on::before{content:"\2713\00a0" / ""}

.tier3-btn:not(.on):hover{background:var(--bg)}
.tier3-btn:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}

/*
 * The read-only form of the same three states, for a viewer with no write
 * rights. Same colours, same words — the screen must not change meaning
 * with permission, only what you may do about it.
 */
.p-primary{background:var(--ok-bg);color:var(--ok-tx)}
.p-secondary{background:var(--price-bg);color:var(--price-tx)}
.p-skip{background:var(--stock-bg);color:var(--stock-tx)}
/* How a row's children are set (components/rules/state-tally.blade.php):
 * the three pills above, small, beside the row's own count. Only drawn on a
 * row whose children are not all set the way it is. */


.state-tally{display:inline-flex;flex-wrap:nowrap;flex-shrink:0;gap:3px;font-weight:400}
.state-tally .pill{padding:0 5px}

/*
 * The drawer's own tier question, colour-matched to the traffic light that
 * opens it. Pressing a green "Primary" segment and landing on a form where
 * "Primary" is plain black is a small break in the chain of "this is the
 * thing I just asked for" — small, and the kind that makes a form feel like
 * a different screen than the one you came from.
 */
.rf-choice-t.t-primary{color:var(--ok-tx)}
.rf-choice-t.t-secondary{color:var(--price-tx)}

/* ---------- the catalogue tree, as columns ----------
 *
 * A .cat-row is a wrapping flex line, so anything that must line up down
 * the page needs a cell of its own with a fixed basis. Three of them were
 * missing, which is why the division tree read as rubble: the state pill
 * and the buttons started wherever the text before them stopped.
 *
 * .cat-state holds the traffic light OR a pill OR nothing at all — a
 * category has no tier, and the empty cell is the point. Closing it up
 * would step every column after it left by a different amount on every
 * row, which is the difference between a table and a list of sentences.
 */
.cat-state{flex:0 0 232px;display:flex;align-items:center;gap:var(--sp-2)}

.tier3{flex:0 0 232px}
.tier3-btn{flex:1 1 auto}

/* Superseded: .cat-count is content-sized above, so there is no width
 * here left to get wrong. Kept as a note rather than a rule — this line
 * was itself the second guess at a number that should never have been
 * one. */


/* .cat-bsrc is unused: the per-row provenance sub-line was removed — it
 * repeated what /change-log keeps, and on a brand row it named the category
 * rather than the brand, reading as if it decided the whole category. Kept
 * as a rule rather than dropped, because the grid it belongs to still has
 * the span-both-columns slot a future second line would want. */

/* One column on a narrow screen — two halves of 260px are worse than one
 * of 520px.
 *
 * 1180px, NOT the 820px .catgrid collapses at, and the difference is
 * deliberate: a cell here carries a name, a count, a pill and up to two
 * buttons, where the dashboard's carries a name, a count and a bar. It
 * runs out of width sooner, so it gives up the second column sooner. */


/* The source line sits under the whole cell at either depth. */


/* The compact traffic light, for a cell in a two-column grid. Same words —
 * shortening them there would put the screen back to two vocabularies,
 * which is the whole thing this control exists to fix — so the padding
 * gives way instead. */
.tier3.sm{flex:0 0 auto}
.tier3.sm .tier3-btn{padding:2px var(--sp-2);font-size:var(--fs-micro)}

/* ---------- the force-list paste box ----------
 * The first <textarea> in the app, and it earns one: a force list arrives
 * as a list — 651 of 715 live entries name a product MPL does not carry, so
 * there is nothing in the catalogue to browse to and the only honest input
 * is the list itself.
 *
 * Monospace, because the person is checking that columns line up before
 * they trust the preview, and a proportional font hides a missing tab.
 * Sized for the length of paste this actually receives (tens of rows, not
 * three), and resizable vertically for the run that is longer.
 */
.fl-paste{
  width:100%;min-height:180px;resize:vertical;
  padding:var(--sp-3);border:1px solid var(--line);border-radius:var(--radius-sm);
  font-family:var(--font-mono);font-size:var(--fs-xs);line-height:1.6;
  color:var(--ink);background:var(--control-bg);
}
.fl-paste:focus-visible{border-color:var(--accent);box-shadow:var(--focus-ring);outline:none}
.fl-paste::placeholder{color:var(--ink3)}


/* ---------- depth, shown as depth ----------
 *
 * Indentation alone was not carrying it: a person looking at seventeen
 * brands could not see that they sat under a sub-category, which sat under
 * a group. The owner asked for colour, getting lighter as it goes deeper,
 * and that is the right instrument — depth is a continuous thing and a
 * ramp reads as continuous where a border reads as a boundary.
 *
 * THE RAMP RUNS TOWARDS THE PAGE, NOT AWAY FROM IT. The group row is the
 * most tinted, each level below it lighter, the brand grid nearly white —
 * so "deeper" reads as "further into the page", and the eye follows the
 * indent instead of fighting it. Every step is a few percent of the accent
 * hue rather than grey: grey at this lightness reads as disabled.
 *
 * Kept deliberately faint. These are backgrounds behind small text that
 * must stay at 4.5:1 on every step, and a tint strong enough to be obvious
 * on its own would fail that before it helped anybody.
 */
:root{
  --depth-1:#e3eafa;   /* group — the row you expanded to get here */
  --depth-2:#eef3fd;   /* sub-category */
  --depth-3:#f7f9ff;   /* brand — nearly the page itself */
}


/* ===================================================== WAREHOUSE MAPPING
 * Which physical stock counts as sellable for a shop.
 *
 * NO NEW HUES. Every colour below is an existing token, on purpose: the
 * action language (the waterfall's eight colours) means "what to do about
 * this product", and nothing on this screen is an action. Introducing a
 * tenth colour here would dilute a language the whole app depends on, and
 * would need its own [data-theme] counterpart to survive the colourblind
 * theme. The one thing that IS marked out — an unsellable or foreign
 * warehouse — is marked by ink weight and a sentence, never by hue alone.
 *
 * The row deliberately looks like .cat-row rather than a form: it is a list
 * of facts you tick, and the stock figure beside each name is what makes an
 * unfamiliar warehouse code legible at all.
 */
/* REGION > BRANCH headings. A disclosure, not a tab strip: there are
 * fifteen regions and eighteen branches in JAKARTA alone, and the point is
 * to open one at a time rather than to choose between them. The tally on
 * each heading is what makes a CLOSED group still informative -- "0 of 22
 * counted" answers the question without opening it. */
.wh-groups{border:1px solid var(--line);border-radius:var(--radius-sm);overflow:hidden;margin-bottom:var(--sp-4)}
.wh-region + .wh-region{border-top:1px solid var(--line)}
/* The heading is a flex ROW now: the disclosure button still fills it, and
 * the All/None pair sits at the end. Those had to live outside the
 * disclosure — a button inside a button is invalid and the inner one never
 * gets the click. */
.wh-region-h,.wh-branch-h{margin:0;display:flex;align-items:center;gap:var(--sp-2)}
.wh-region-h > .wh-disc,.wh-branch-h > .wh-disc{flex:1;min-width:0}

/* ===== WAREHOUSE BULK TICKS
 * 271 columns, and giving one shop its own mapping used to be an individual
 * press per box. These move ticks only; Apply still refuses without a fresh
 * measurement. */
.wh-bulk{
  display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);
  padding:var(--sp-2) var(--sp-3);margin-bottom:var(--sp-2);
  background:var(--inset);border:1px solid var(--line2);border-radius:var(--radius);
}
.wh-bulk-count{font-size:var(--fs-micro);color:var(--ink3)}
.wh-bulk-actions{display:flex;gap:var(--sp-2);margin-left:auto;flex-wrap:wrap}
/* THE HEADING CHECKBOX (1 Oct 2026) — Shopify's and Quicken's group
 * checkbox, which replaced the All/None pair: one press ticks or clears a
 * whole region or branch, open or closed. It sits in the heading's own band,
 * so the band carries the depth colour rather than only the button. */
.wh-region-h{background:var(--depth-2)}
.wh-branch-h{background:var(--depth-3)}
.wh-head-tick{flex-shrink:0;margin:0 0 0 var(--sp-4)}
.wh-branch-h > .wh-head-tick{margin-left:var(--sp-6)}
.wh-head-tick + .wh-disc{padding-left:0}
.wh-search{flex:1;min-width:12rem;max-width:22rem}

.wh-disc{display:flex;align-items:baseline;gap:var(--sp-3);width:100%;text-align:left;
  appearance:none;border:0;background:var(--depth-2);cursor:pointer;font-family:inherit;
  padding:var(--sp-3) var(--sp-4);font-size:var(--fs-sm);font-weight:700;color:var(--ink)}
.wh-disc:hover{background:var(--depth-1)}
.wh-disc:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}

/* The branch level sits one step in and one step quieter, so the two
 * levels are told apart by depth rather than by colour alone. */
.wh-disc-sm{background:var(--depth-3);padding-left:var(--sp-6);font-weight:600;font-size:var(--fs-xs)}
.wh-branch-h + .wh-list{border-top:1px solid var(--line2)}
.wh-branch{border-top:1px solid var(--line2)}

/* BRANCHES TWO PER ROW, for the same reason the warehouses are: JAKARTA
 * alone has eighteen of them and they are one-line headings.
 *
 * The region heading spans both columns — it is the thing the two columns
 * are under — and so does an OPEN branch, because the warehouse list inside
 * it is itself two columns and would otherwise be crushed into half the
 * width. `wh-branch-open` comes from the Blade, not a :has(), because it is
 * a layout decision and deserves to be stated. */
.wh-region{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr))}
.wh-region-h{grid-column:1 / -1}
.wh-branch-open{grid-column:1 / -1}

/* NO vertical seam between branches, deliberately — unlike the warehouse
 * rows. A full-width open branch pushes the next one onto a fresh row, so
 * nth-of-type parity stops matching which COLUMN a branch is actually in and
 * the seam would start drawing down the middle of the left-hand one. The
 * per-branch top border already separates them. */

@media (max-width: 900px){
  .wh-region{grid-template-columns:1fr}
}

.wh-disc-mark{flex:0 0 auto;color:var(--ink3);font-size:var(--fs-micro)}
.wh-disc-name{font-family:var(--font-mono);letter-spacing:-.01em}

/* Pushed right so the numbers line up down the column and can be compared
 * between headings without reading the names. */
.wh-disc-tally{margin-left:auto;font-weight:400;font-size:var(--fs-micro);color:var(--ink2);
  font-variant-numeric:tabular-nums}

/* "this branch" -- the store's own, which is where the 25 columns anybody
 * actually maps live. A word, not a colour: it has to survive the
 * colourblind theme and a greyscale print. */
.wh-here{font-size:var(--fs-micro);font-weight:600;color:var(--ok-tx);background:var(--ok-bg);
  padding:1px var(--sp-2);border-radius:var(--radius-xs)}

/* ===== TWO UP (owner, 17 Sep 2026: "display the options side by side. 2
 * warehouses and 2 branches in 1 row")
 *
 * 268 warehouse columns down a single file is a lot of scrolling for a
 * screen whose whole job is "which of these count?". Two columns halves it,
 * and the rows are short enough that nothing wraps at the widths this app is
 * used at.
 *
 * `minmax(0,1fr)`, not `1fr`: a grid track's default minimum is auto, so a
 * long warehouse code would push the column wider than its share instead of
 * being contained by it.
 *
 * One column below 900px. The picker is browsed on a laptop, but the rest of
 * this app is expected to survive a tablet, and two columns of code + counts
 * do not. */
.wh-list{
  overflow:hidden;
  display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));
}
.wh-row{display:flex;align-items:flex-start;gap:var(--sp-3);padding:var(--sp-3) var(--sp-4);border-bottom:1px solid var(--line2)}
/* In two columns "last child" is no longer "last row", so the border is
 * dropped from the final PAIR instead — otherwise one stray hairline hangs
 * under the left column. */
.wh-row:last-child,.wh-row:nth-last-child(2):nth-child(odd){border-bottom:0}
.wh-row:hover{background:var(--bg)}
/* The seam between the two columns, so a tick is unambiguously attached to
 * the name on its own side. */
.wh-row:nth-child(even){border-left:1px solid var(--line2)}

@media (max-width: 900px){
  .wh-list{grid-template-columns:1fr}
  .wh-row:nth-child(even){border-left:0}
  .wh-row:nth-last-child(2):nth-child(odd){border-bottom:1px solid var(--line2)}
}

/* A real checkbox, sized to be a comfortable target rather than restyled
 * into something that only looks like one. */
.wh-tick{width:16px;height:16px;margin-top:2px;flex:0 0 auto;cursor:pointer}
.wh-tick:disabled{cursor:not-allowed}

.wh-label{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--sp-2) var(--sp-3);flex:1 1 auto;cursor:pointer}
.wh-name{font-family:var(--font-mono);font-size:var(--fs-sm);font-weight:600;color:var(--ink)}
.wh-stock{font-size:var(--fs-xs);color:var(--ink2);font-variant-numeric:tabular-nums}

/* The reason a row cannot be ticked, or should not be. A sentence rather
 * than a colour, because "why is this greyed out?" is the question a colour
 * alone always leaves behind. */
.wh-why{font-size:var(--fs-micro);color:var(--ink3);font-style:italic}
.wh-absent{font-size:var(--fs-xs);color:var(--ink3);font-style:italic}

.wh-unsellable .wh-name{color:var(--ink3);text-decoration:line-through}
.wh-unsellable{background:var(--bg)}


/* The measurement. Given room, because it is the one thing on this screen
 * that has to be read rather than skimmed. */
.wh-apply{border-top:2px solid var(--line);padding-top:var(--sp-4);margin-top:var(--sp-4)}
.wh-measure{min-height:2.5em;margin-bottom:var(--sp-3)}
.wh-headline{font-size:var(--fs-md);line-height:1.5;margin:0 0 var(--sp-2)}
.wh-detail{font-size:var(--fs-sm);color:var(--ink2);margin:0 0 var(--sp-2)}
.wh-nochange{font-size:var(--fs-sm);color:var(--ink2);margin:0}

.wh-actions{display:flex;align-items:center;gap:var(--sp-3);flex-wrap:wrap}
.wh-actions label{display:flex;align-items:center;gap:var(--sp-2);font-size:var(--fs-sm)}
.wh-actions input[type=text]{width:8em}

@media(max-width:820px){
  .wh-row{flex-wrap:wrap}
  .wh-actions{align-items:flex-start;flex-direction:column}
}

/* ===== CODE LINK
 *
 * A KODEBARANG or store code that is also a way in. `.kode` alone paints
 * ink-coloured monospace, which is right for a code being read and wrong for
 * one that can be clicked — with no global anchor rule in this file, a bare
 * <a class="kode"> looks exactly like the text beside it.
 *
 * Accent colour plus underline-on-hover, both tokens, so the colourblind
 * theme follows without anything added there. Focus is already handled by
 * the shared a:focus-visible rule.
 */
.kode-link{color:var(--accent);text-decoration:none}
.kode-link:hover{text-decoration:underline}

/* ===== TASK STEPS
 *
 * A numbered sequence for a screen where the job has an ORDER — today only
 * the Force list intake: decide where it applies, paste the list, preview
 * and apply. That screen used to be one flat panel whose stages were
 * distinguished by two small grey field labels, and the owner's reading of
 * it was the fair one: the steps did not look like steps, and nothing said
 * what happened after Preview.
 *
 * Real <ol>/<li> underneath, so the sequence is in the markup rather than
 * implied by three circles — a screen reader announces "list, 3 items" and
 * each step's number comes from the element, not from a decorative span.
 *
 * TOKENS ONLY, NO NEW HUE, which is why the colourblind block below needs
 * nothing added for it: every colour here is one the theme already
 * re-points.
 */
.steps{list-style:none;margin:0;padding:0;counter-reset:step}
.steps > li{position:relative;counter-increment:step;padding:0 0 var(--sp-5) var(--sp-8);border-left:2px solid var(--line2);margin-left:13px}
.steps > li:last-child{border-left-color:transparent;padding-bottom:0}
.steps > li::before{
  content:counter(step);
  position:absolute;left:-14px;top:-2px;
  width:26px;height:26px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  background:var(--panel);border:2px solid var(--line);
  font-size:var(--fs-xs);font-weight:700;color:var(--ink3);
}
/* The step you can act on right now. Not colour alone: the ring thickens
   and the number goes to the accent ink, so the cue survives greyscale. */
.steps > li.is-now::before{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}
/* A step that cannot be acted on yet reads as dimmed, and says why in its
   own body text rather than being hidden — hiding it is what made the old
   screen feel like it ended at the Preview button. */
.steps > li.is-waiting{opacity:.55}
.steps .step-h{font-size:var(--fs-sm);font-weight:650;color:var(--ink);margin:0 0 var(--sp-1) 0;display:flex;align-items:center;gap:var(--sp-2);min-height:24px}
.steps .step-sub{font-size:var(--fs-xs);color:var(--ink3);margin:0 0 var(--sp-3) 0}
.steps .step-body{margin:0}

/* The collapsed example. <details>/<summary> so it works with no JS and the
   toggle is a real control; the marker is left alone rather than replaced,
   because a custom one has to re-implement the open/closed state it already
   has. */
.ex-toggle{margin:0 0 var(--sp-3) 0}
.ex-toggle > summary{cursor:pointer;font-size:var(--fs-xs);color:var(--accent);font-weight:600;padding:var(--sp-1) 0;width:fit-content}
.ex-toggle > summary:focus-visible{outline:none;box-shadow:var(--focus-ring);border-radius:var(--radius-xs)}
.ex-toggle[open] > summary{margin-bottom:var(--sp-2)}

@media(max-width:820px){
  .steps > li{padding-left:var(--sp-6)}
}

/* ===== QUEUE STATUS
 *
 * Whether a rule saved right now will actually reach the dashboards. Lives
 * in the topbar next to the "Data as of" stamp because the two answer the
 * same shape of question about the two halves of the pipeline — did
 * tonight's data arrive, and is today's rule being applied — and they
 * should be read in one glance.
 *
 * CALM WHEN FINE. A healthy queue is one short pill and no sentence. The
 * explanatory line only renders when something is wrong, for the same
 * reason the freshness banner is a small stamp until it is a red strip: a
 * topbar that narrates its own health all day stops being read, and this
 * one has to be read on the day it turns red.
 *
 * *** A SECOND BORROWING OF THE ACTION HUES, recorded here so it is a
 * decision and not a drift. *** This file's header notes one already: the
 * listing-rules tier control spends OK's green, UPDATE PRICE's amber and
 * UPDATE STOCK's red on "how much does this slice count". This is the
 * second, spending the same three on "is the pipeline moving". The reason
 * it survives is the same one given there — the two languages are never
 * read as one sentence. An action pill answers a question about one product
 * on a worklist; this answers a question about the machine, and it lives in
 * the chrome, never inside a table and never beside an action pill. If a
 * third component reaches for these hues, or if this one ever moves into
 * the content well, revisit all three together.
 *
 * TOKENS ONLY, NO NEW HUE, which is why the colourblind block below needs
 * nothing added for it: every colour here is one the theme already
 * re-points. In that theme the three states collapse to near-identical
 * greys, which is exactly why the pill always carries its WORD and the dot
 * is aria-hidden decoration rather than the signal.
 */

/* The topbar's right-hand group. The topbar is `justify-content:
   space-between` over two children; this keeps it two by holding the
   freshness stamp and the pill together, rather than adding a third child
   and pushing the stamp into the middle of the bar. */
.topbar-right{display:flex;align-items:center;gap:var(--sp-4);flex-shrink:0}
/* The title block keeps first claim on the width. Without this the status
   note pushes "Store dashboard" into a two-line wrap the moment the queue
   has something to say. */
.topbar > div:first-child{flex:1;min-width:0}

.qstatus{display:flex;flex-direction:column;align-items:flex-end;gap:var(--sp-1)}
.qstatus-row{display:flex;align-items:center;gap:var(--sp-2)}

.qstatus-pill{
  display:inline-flex;align-items:center;gap:var(--sp-2);
  padding:3px 9px;border-radius:20px;
  font-size:var(--fs-micro);font-weight:600;white-space:nowrap;letter-spacing:.01em;
}
.qstatus-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}

/* The three states. The -dot tokens are SHAPES, never text grounds (see the
   tokens block) — so the ground is the -bg tint and the word takes the -tx
   ink, which is the pairing that clears AA. */
.qstatus-pill.is-idle{background:var(--ok-bg);color:var(--ok-tx)}
.qstatus-pill.is-idle .qstatus-dot{background:var(--ok-dot)}
.qstatus-pill.is-working{background:var(--price-bg);color:var(--price-tx)}
.qstatus-pill.is-working .qstatus-dot{background:var(--price-dot)}
/* Down and Failed share the urgent pair rather than UPDATE STOCK's red:
   this file already reserves --urgent-* for "refusals and failures", which
   is what both of these are. */
.qstatus-pill.is-down{background:var(--urgent-bg);color:var(--urgent-tx)}
.qstatus-pill.is-down .qstatus-dot{background:var(--stock-dot)}

/* *** NO `display` RULE ON THE BUTTON'S TWO FACES. *** They carry
   wire:loading / wire:loading.remove, and Livewire hides them with a
   `[wire\:loading][wire\:loading]{display:none}` rule of its own plus inline
   styles. A `.qstatus .btn-sm > span{display:inline-flex}` here is MORE
   specific than that (0,2,1 against 0,2,0), so it wins and both faces render
   at once — the button read "Start … Starting…" side by side. This is the
   same trap the LOADING FEEDBACK section's own note warns about; the fix is
   to leave display alone and align the glyph with vertical-align, exactly as
   the existing Save buttons do. */
.qstatus .btn-sm{white-space:nowrap}
/* .ico is only sized inside .nav, so the button's glyph needs its own box
   here rather than inheriting one it never had. inline-block, not block, so
   it sits on the text baseline inside an inline span. */
.qstatus .ico{width:13px;height:13px;display:inline-block;vertical-align:-2px;margin-right:2px}

/* The sentence. Capped and right-aligned so a long refusal from schtasks
   wraps inside the topbar instead of stretching it and squeezing the page
   title next to it. */
.qstatus-note{font-size:var(--fs-xs);color:var(--ink3);text-align:right;max-width:38ch}
.qstatus-note.is-bad{color:var(--urgent-tx)}

@media(max-width:820px){
  /* Below this width the topbar's two groups stack; the pill keeps its own
     line rather than being squeezed against the page title. */
  .topbar{flex-wrap:wrap}
  /* ...on a line of its own that is allowed to SHRINK. The wrap alone did
     nothing while `flex-shrink:0` above held the group at its content
     width: 538px on a 375px phone, which widened every page into a
     sideways scroll (UI review, 26 Sep 2026). */
  .topbar-right{flex-wrap:wrap;justify-content:flex-start;flex-shrink:1;min-width:0;width:100%}
  .qstatus-note{max-width:100%}
}

/* ===== FORCED PRICE AUDIT — the two owner action items, on a screen
 *
 * Inside an .alert-error, so it inherits that block's ground and ink rather
 * than introducing a fifth way of saying "this needs attention". The table
 * is deliberately small and capped at ten rows: this is a prompt to go and
 * look, not the report itself — mpl:check-control-lists still prints every
 * row for anyone who wants them all. */
.fpa-codes{margin-top:var(--sp-2);display:flex;gap:var(--sp-2);flex-wrap:wrap}
.fpa-table{width:100%;margin-top:var(--sp-3);border-collapse:collapse;font-size:var(--fs-xs)}
.fpa-table th{
  text-align:left;font-weight:700;padding:2px var(--sp-2);
  border-bottom:1px solid var(--urgent-line);
}
.fpa-table th.num,.fpa-table td.num{text-align:right;font-variant-numeric:tabular-nums}
.fpa-table td{padding:2px var(--sp-2)}

/* ===== STORE HERO: MOVEMENT — REMOVED 22 Sep 2026
 *
 * `.hero-move`, `.hero-delta`, `.hero-move-check` and their two theme
 * overrides are gone. They painted the store dashboard's movement line —
 * "▲ 14 points since 2 Sep" on the dark hero panel — which was dropped when
 * the completion-over-time chart began rendering for every role: the line
 * only ever drew where there was no chart AND a comparison to make, and
 * those cannot both be true. See the note in the store dashboard's Blade
 * where the markup used to be.
 *
 * *** THE INCIDENT THEY CAUSED IS KEPT ON PURPOSE, IN
 * HeroPanelThemeTest's DOCBLOCK. *** These rules are why that test exists:
 * they were added in a correctly-placed section, their author thought about
 * the colourblind theme and handled the SEMANTIC channel (a ▲ glyph and the
 * word "points") while missing the CONTRAST one, and the result was pale
 * pink on near-white at 1.91:1 — invisible to anyone on the default theme.
 * The test is generic over `.hero-*`, so it outlives the rules that prompted
 * it and still guards every remaining one. */

/* ===== STORE TREND — completion over time
 *
 * THIN MARKS, HAIRLINE GRID, NO CHART LIBRARY. The app has no build step and
 * its own header forbids a CDN, so this is inline SVG styled from the same
 * tokens as everything else — which is also what makes it follow all three
 * themes without a per-theme chart palette.
 *
 * ONE SERIES, so no legend: the heading names it. Completion wears the OK
 * green it already wears in the hero figure and the pile bar, because a
 * third colour for the same idea would be a third vocabulary. The
 * denominator line below is deliberately MUTED INK rather than a second hue
 * — it is context for the line above, not a rival series.
 *
 * Colour is never the only channel: direction is stated in words beside the
 * figure, rule-change days carry a ⚙ glyph as well as a rule, and the whole
 * thing has a table twin.
 *
 * The grid is SOLID. A dashed gridline reads as a threshold or a projection
 * when it is only a grid. */
.trend-head{
  display:flex;align-items:baseline;gap:var(--sp-3);flex-wrap:wrap;
  padding:var(--sp-4) var(--sp-5) 0;
}
/* Proportional figures, not tabular: equal-width digits make a large
 * standalone number look loose. Tabular belongs in the table below. */
.trend-now{font-size:var(--fs-xl);font-weight:700;color:var(--ink)}
.trend-delta{font-size:var(--fs-sm);font-weight:700;white-space:nowrap}
.trend-delta.is-up{color:var(--ok-tx)}
.trend-delta.is-down{color:var(--stock-tx)}

.trend-fig{margin:0;padding:var(--sp-3) var(--sp-5) 0}
/* The store dashboard's chart section is a panel (26 Sep 2026). */
.trend-panel{margin-top:var(--sp-5)}
.trend-panel > .empty{padding:var(--sp-5)}

/* ===== THE TWO PLOTS, SIDE BY SIDE
 *
 * Completion on the left, input products on the right (owner, 16 Sep 2026).
 * They were stacked, each running the panel's full width, and on a 1,990px
 * monitor that painted a 1,000-unit drawing across twice its width: every
 * slope half as steep as it was drawn, which is the shape people read a
 * trend by. Two columns put the horizontal scale back at roughly 1:1 and
 * cost nothing — neither plot was using the room.
 *
 * minmax(0,1fr), not 1fr: a grid track's default min-width is auto, and the
 * SVG inside would then refuse to shrink below its intrinsic width and push
 * the column past its share.
 *
 * They stack again below 1400px, and that number is measured rather than
 * borrowed from the 1180 the tiles above reflow at. What has to fit is TEN
 * point labels side by side: at 1180 each column comes out about 440px, the
 * labels are ~30px each, and ten of them left ~11px of gap — "75% 75%" ran
 * together at the right-hand end. 1400 gives each column ~550px, which is
 * the narrowest that still reads. Stacked full-width is a perfectly good
 * fallback, so it is worth being conservative about which side of it a
 * middling screen lands on. */
.trend-charts{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:0 var(--sp-8)}
@media(max-width:1400px){
  .trend-charts{grid-template-columns:minmax(0,1fr)}
}

.trend-svg{display:block;width:100%;height:220px;overflow:visible}
/* THE HEIGHT HERE MUST EQUAL THE viewBox HEIGHT StoreTrend::geometry()
 * RETURNS — 'h' for the plot above, 'subH' for the one beside it, and since
 * 16 Sep 2026 those are the same number because the two plots are peers.
 * preserveAspectRatio="none" scales x and y independently, and the label
 * layer positions itself VERTICALLY IN PIXELS on the strength of that
 * vertical scale being exactly 1. Break the pairing and every number on the
 * plot sits at the wrong height, silently.
 *
 * .trend-svg-sub carries no rule of its own any more and is deliberately
 * still in the markup: it used to set this plot's smaller height and a 2px
 * gap from the chart stacked above it. Side by side there is nothing above
 * it, and that 2px was pure misalignment — two charts meant to be read
 * across a gap with their baselines 2px apart. The class stays as the hook
 * that tells the two plots apart. */

/* ===== THE CHART'S LABEL LAYER
 *
 * Every number on these two plots is HTML sitting on top of the SVG, never
 * SVG <text> — see the long note in components/store/completion-trend.
 * Short version: preserveAspectRatio="none" is what lets the chart fill the
 * panel at a fixed height, and it stretches glyphs along with the line. At
 * 1,990px the viewBox's 1000 units scale by 1.99x and every figure was drawn
 * two-thirds as wide again as it was tall.
 *
 * Horizontal position is a PERCENTAGE (the same mapping the SVG applies to
 * its own x units); vertical is PIXELS (exact, because the vertical scale is
 * pinned at 1 above). pointer-events:none so this layer cannot steal a hover
 * from the invisible targets underneath it. */
.trend-plot{position:relative}
.trend-labels{position:absolute;inset:0;pointer-events:none}
.trend-labels > *{position:absolute;white-space:nowrap;transform:translate(-50%,-50%)}
/* The two ends pull inside the plot rather than centring past its edge. */
.trend-labels > .at-start{transform:translate(0,-50%)}
.trend-labels > .at-end{transform:translate(-100%,-50%)}

.trend-grid{stroke:var(--line2);stroke-width:1}
/* Right-aligned against the plot's left edge whatever the number's width,
 * so the axis reads as a column. */
.trend-labels > .trend-tick{
  color:var(--ink3);font-size:var(--fs-micro);font-variant-numeric:tabular-nums;
  transform:translate(-100%,-50%);
}

.trend-line{fill:none;stroke:var(--ok-tx);stroke-width:2.5;stroke-linejoin:round;stroke-linecap:round}
/* A 2px surface ring, so a dot that lands on the grid or on another mark
 * still reads as one object rather than merging with it. */
.trend-dot{fill:var(--ok-tx);stroke:var(--panel);stroke-width:2}

/* INPUT PRODUCT's own hue since 26 Sep 2026 (design prototype): the grey was
 * chosen when this line was a denominator shown as context; it now plots the
 * INPUT PRODUCT pile, and that pile already wears this blue on its tile, its
 * pill and its sidebar dot — the same language, not a new hue. */
.trend-line-sub{fill:none;stroke:var(--input-dot);stroke-width:2;stroke-linejoin:round;stroke-linecap:round}
.trend-dot-sub{fill:var(--input-dot);stroke:var(--panel);stroke-width:1.5}

/* The rule-change marker: a hairline plus a glyph. Not the urgent hue — a
 * supervisor doing their job is not a fault, which is the same reasoning
 * .delta-rule carries on the division table. */
.trend-mark{stroke:var(--ink3);stroke-width:1;opacity:.35;stroke-dasharray:2 3}
.trend-mark-glyph{color:var(--ink3);font-size:var(--fs-micro);line-height:1}

/* Tabular figures HERE, unlike the big headline above: these are a column of
 * numbers the eye runs along a line, and a "1" that is narrower than a "7"
 * makes two adjacent points look unevenly spaced when they are not. */
.trend-endlabel{color:var(--ink2);font-size:var(--fs-xs);font-weight:700;font-variant-numeric:tabular-nums}

/* The lower plot's per-point numbers. Quieter than .trend-endlabel on
 * purpose — same hue as that line and its dots (--ink3), a step smaller, and
 * not bold: completion is the figure people are judged by and must stay the
 * loudest thing in this panel. */
.trend-sublabel{color:var(--ink3);font-size:var(--fs-micro);font-weight:600;font-variant-numeric:tabular-nums}

/* The hover/focus target. Invisible, 48px wide, and a real focus stop — the
 * dot is 4px and nobody should have to hit that with a mouse, let alone
 * reach it with a keyboard. */
.trend-hit rect{fill:transparent;cursor:pointer}
.trend-hit:focus{outline:none}
/* The box-shadow ring does not render on an SVG <g>, and the soft fill
 * below is ~1.1:1, so a keyboard user tabbing through the chart could not
 * see which point they were on (24 Sep 2026). A stroke can be seen. */
.trend-hit:focus-visible rect{stroke:var(--accent);stroke-width:2}
.trend-hit:focus rect,.trend-hit:hover rect{fill:var(--accent-soft);opacity:.55}

/* Each column names its own plot. Stacked, the panel heading and the big
 * percentage above could speak for the upper chart and only the lower one
 * needed naming; side by side, a reader who meets the right-hand plot first
 * has to be told what it is without looking left. */
.trend-sub-label{
  font-size:var(--fs-xs);color:var(--ink2);font-weight:600;
  margin-top:var(--sp-3);
}
/* BOTH labels keep the same top margin, including the first. Zeroing it on
 * the left column to close the gap under the headline read as a tidy-up and
 * was a misalignment: it lifted the left plot by 12px, so two charts meant to
 * be compared across a gap had their baselines at different heights. */
@media(max-width:1400px){
  /* Stacked again, the second column needs the gap back. */
  .trend-col + .trend-col{margin-top:var(--sp-5)}
}
.trend-axis{
  display:flex;justify-content:space-between;
  font-size:var(--fs-xs);color:var(--ink3);padding-top:2px;
}

.trend-table{margin:var(--sp-4) var(--sp-5) var(--sp-5)}
.trend-table summary{
  cursor:pointer;font-size:var(--fs-sm);color:var(--accent);font-weight:600;
}
.trend-table table{margin-top:var(--sp-3)}

/* ===== DONE MARKS — "I have done this one"
 *
 * The first thing in this app a staff member can record about their own
 * work. An ANNOTATION, never a verdict: it cannot move a row out of its pile
 * or shift a percentage by a point. See the worklist_touches migration.
 *
 * QUIET WHEN UNMARKED, OBVIOUS WHEN MARKED. Every open row carries one of
 * these, so an unmarked tick has to sit below the noise floor of a table
 * already carrying a Copy button and eight columns — it is --ink3 on
 * nothing, the same weight as the em dash it replaces. A MARKED one is the
 * OK green, because "done" already means that hue everywhere else in this
 * app and a second green would be a second language.
 *
 * The tick is a SHAPE as well as a colour — ○ becomes ✓ — so it survives the
 * colourblind theme, where --ok-tx and --ink3 are two greys. Same reason the
 * queue pill spells its state out in words. */
.tick{
  background:none;border:none;padding:2px var(--sp-2);cursor:pointer;
  font-size:var(--fs-md);line-height:1;color:var(--ink3);
  border-radius:var(--radius-xs);
}
.tick:hover{color:var(--ok-tx);background:var(--ok-bg)}
.tick.on{color:var(--ok-tx);font-weight:700}
.tick:disabled{opacity:.5;cursor:progress}

/* Who marked it. Names, not initials: the collision this exists to prevent
 * is two people on one store, and "somebody already did this" is only
 * useful if it says who. */
.tick-who{
  font-size:var(--fs-micro);color:var(--ink3);
  margin-top:1px;white-space:nowrap;
}

/* "47 of 871 marked done", beside the pile heading. Deliberately not a
 * progress BAR: a bar invites comparison with the completion figure below
 * it, and these two numbers mean different things — one is what a person
 * says they did, the other is what the engine confirmed. */
.tick-progress{
  margin-left:var(--sp-3);font-size:var(--fs-xs);font-weight:400;
  color:var(--ok-tx);background:var(--ok-bg);
  padding:2px var(--sp-2);border-radius:var(--radius-xs);
  white-space:nowrap;
}

/* ===== SCOPE IDENTITY — division-wide, or one shop?
 *
 * Four authoring screens come in DIVISION/STORE pairs, each pair one
 * component asked of two audiences. They behave identically on purpose and
 * used to LOOK identical too, which is a safety problem rather than a
 * cosmetic one: the owner set rules believing they were on Stores and were
 * on Divisions (12 Sep 2026), and these screens save on the press with no
 * confirmation step. App\Support\PageScope carries the full reasoning.
 *
 * WHY STICKY AND NOT JUST A LOUDER HEADING. The topbar is not pinned — it
 * scrolls away — so once you are down among the category rows, which are
 * identical on both screens, the only cue left is a small highlight in the
 * rail. The mistake happens exactly there, so the marker has to still be
 * there.
 *
 * WHY IT IS RENDERED OUTSIDE .content. That element has overflow:auto and
 * never scrolls (the body does), which is still enough to make it the
 * nearest scrolling ancestor — so position:sticky inside it pins to a
 * scrollport that never moves and does nothing, with no error and nothing
 * to see. Measured in a browser. .main has no overflow; the strip lives
 * there.
 *
 * THE HUES ARE BORROWED ON PURPOSE, from .scope-tag above: store = the
 * INPUT family, division = the SECONDARY family. Scope already means those
 * colours elsewhere in this app, and a fifth private colour language would
 * be worse than a borrowed one. Safe here because these screens show no
 * action pills — the tier control below them is green/amber/red, which does
 * not collide. Colour is the SECOND channel only: the strip always spells
 * the scope out in words. */
[data-scope="division"]{
  --scope-bg:var(--sec-bg); --scope-tx:var(--sec-tx); --scope-edge:var(--sec-dot);
}
[data-scope="store"]{
  --scope-bg:var(--input-bg); --scope-tx:var(--input-tx); --scope-edge:var(--input-dot);
}

.scope-strip{
  position:sticky;top:0;z-index:20;
  display:flex;align-items:baseline;gap:var(--sp-3);flex-wrap:wrap;
  padding:var(--sp-2) var(--sp-6);
  background:var(--scope-bg);color:var(--scope-tx);
  border-bottom:1px solid var(--scope-edge);
  box-shadow:inset 5px 0 0 var(--scope-edge);
}
.scope-kind{
  font-size:var(--fs-micro);font-weight:700;
  text-transform:uppercase;letter-spacing:.06em;
  display:inline-flex;align-items:center;gap:var(--sp-2);flex-shrink:0;
}
/* A dot, not an icon: it is the same marker shape the rail already uses for
 * an action, and it survives the colourblind theme as a shape. */
.scope-kind::before{
  content:"";width:8px;height:8px;border-radius:50%;
  background:var(--scope-edge);flex-shrink:0;
}
.scope-note{font-size:var(--fs-xs)}

/* The identity carries down the page as well as across the top, so a short
 * viewport scrolled past the strip still shows which scope is being edited. */
[data-scope] .content .panel{border-left:3px solid var(--scope-edge)}

/* ===== PRODUCT COVERAGE
 *
 * The paste-codes screen: one row per code, expanding to a chip per store.
 *
 * NO NEW HUE. Every colour here is an existing action token pair, so all
 * three themes already carry counterparts — there is nothing for a new theme
 * to forget. That is deliberate: the last palette pass found 32 literals no
 * theme could reach.
 *
 * *** A GAP IS RED HERE, NOT THE BLUE OF THE ACTION THAT FIXES IT (owner,
 * 14 Sep 2026). *** `.is-missing` used to borrow the INPUT PRODUCT pair, on
 * the rule that "a coverage chip and a worklist pill mean the same thing in
 * the same colour". The owner rejected the result on sight: *"it's not very
 * obvious through the color which is listed, which store has problems ...
 * the color has to be obvious and intuitive too."*
 *
 * THE RULE THAT PRODUCED IT IS RIGHT EVERYWHERE ELSE, AND THAT IS WHY IT
 * FAILED HERE. The action palette above puts every `-bg` in a narrow
 * lightness band on purpose, "so no pill shouts louder than another for
 * reasons unrelated to its meaning" — correct for a worklist, where the
 * actions are peers and the reader is working down a list. This grid is not
 * a list of peers: it is 31 squares read at a glance to answer ONE question,
 * which stores have a problem. Here the loudness IS the meaning, and a
 * palette that equalises it removes the only thing being asked.
 *
 * So: green = fine, red = somebody must act, grey = nobody's problem. The
 * cost, recorded honestly, is that a missing listing is red here and blue on
 * the worklist. That correspondence was worth less than being readable.
 *
 * TWO CHANNELS, NEVER ONE. The GLYPH says whether a listing exists and the
 * COLOUR says whether its absence is anybody's problem — a store held by a
 * whitelist or a stock floor has no listing either, and painting that like
 * a real gap would report a supervisor's own decision back as a failure.
 * `.is-deliberate` is what keeps those apart. Colour is therefore never the
 * only channel, which is also what makes the grid survive greyscale — and in
 * Colourblind, which is near-greyscale by design, the glyph is doing ALL of
 * this work. That theme is deliberately untouched by this change.
 */
.cov-row{border-bottom:1px solid var(--line);padding:var(--sp-2) 0}
.cov-row:last-child{border-bottom:none}

/* The header line is the expander PLUS the copy controls beside it. They are
 * siblings because a <button> cannot contain a <button>; this row is what
 * puts them on one line. */
.cov-head-row{display:flex;align-items:center;gap:var(--sp-2)}

/* A real <button>: the header line is the expander, so it must be reachable
 * and announced as one control, not a div with a click handler.
 *
 * `flex:1;min-width:0` rather than the `width:100%` this had before it gained
 * a sibling — at 100% it claims the whole row and squeezes the copy buttons
 * to nothing, and min-width:0 is what lets .cov-name's ellipsis still work
 * inside a flex child. */
.cov-head{
  display:flex;align-items:center;gap:var(--sp-3);flex:1;min-width:0;
  background:none;border:none;padding:var(--sp-2);text-align:left;
  cursor:pointer;border-radius:var(--radius-xs);color:inherit;font:inherit;
}
.cov-head:hover{background:var(--row-hover)}

/* COPY CODE / COPY NAME. Quiet until wanted — they sit on every row, and 31
 * rows of loud buttons would compete with the answer the screen is for — but
 * NOT hover-only: a control that only exists on hover cannot be found on a
 * touch screen and cannot be tabbed to with any confidence. */
.cov-copy-group{display:inline-flex;align-items:center;gap:4px;flex-shrink:0}
.cov-copy{
  display:inline-flex;align-items:center;gap:4px;
  padding:2px var(--sp-2);border-radius:var(--radius-xs);
  border:1px solid var(--line);background:var(--control-bg);color:var(--ink2);
  font-size:var(--fs-micro);font-weight:600;cursor:pointer;white-space:nowrap;
}
.cov-copy:hover{background:var(--hover-strong);color:var(--ink)}
.cov-copied{font-size:var(--fs-micro);color:var(--ok-tx);min-width:0;white-space:nowrap}
.cov-twisty{color:var(--ink3);width:12px;flex-shrink:0}
.cov-kode{font-weight:600;flex-shrink:0}
.cov-name{color:var(--ink2);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cov-bar{display:flex;align-items:center;gap:var(--sp-2);flex-shrink:0}
.cov-verdict{font-size:var(--fs-xs);color:var(--ink2);flex-shrink:0}

.cov-extra{display:flex;flex-wrap:wrap;gap:6px;padding:0 var(--sp-2) var(--sp-2) 30px}

.cov-stores{padding:var(--sp-2) var(--sp-2) var(--sp-3) 30px}
.cov-div + .cov-div{margin-top:var(--sp-3)}
.cov-div-head{
  font-size:var(--fs-micro);font-weight:600;text-transform:uppercase;
  letter-spacing:.03em;color:var(--ink3);margin-bottom:6px;
  display:flex;gap:var(--sp-2);align-items:baseline;
}
/* Wraps rather than assuming a column count: dim_store went 14 -> 31 on
 * 10 Sep 2026 and is not a fixed number. */
.cov-grid{display:flex;flex-wrap:wrap;gap:6px}
/* The transparent border is load-bearing: .is-blacklisted turns it visible,
 * and without it here that one chip would be 2px wider and 2px taller than
 * its neighbours and shunt the whole wrapped grid along. */
.cov-chip{
  display:inline-flex;align-items:center;gap:6px;
  padding:3px 9px;border-radius:20px;
  border:1px solid transparent;
  font-size:var(--fs-micro);font-weight:600;white-space:nowrap;
  background:var(--line2);color:var(--ink2);
}
.cov-store{font-variant-numeric:tabular-nums}
.cov-cell{display:inline-block;width:1em;text-align:center;font-weight:700}

.cov-chip.is-listed{background:var(--ok-bg);color:var(--ok-tx)}
/* RED, not the INPUT PRODUCT blue it shipped with — see the note above. */
.cov-chip.is-missing{background:var(--urgent-bg);color:var(--urgent-tx)}
/* Blacklisted shares the red, because both are "somebody must act", and is
 * told apart two ways that do not rely on hue: the GLYPH (a blacklisted
 * store that HAS the listing still reads ✓, so ✓-in-red is "take it down"
 * and ✗-in-red is "put it up"), and this darker edge. --urgent-line-strong
 * rather than --urgent-line for the reason that token exists at all: an
 * urgent border INSIDE an urgent fill is 1.23:1 and disappears. */
.cov-chip.is-blacklisted{
  background:var(--urgent-bg);color:var(--urgent-tx);
  border-color:var(--urgent-line-strong);
}
/* Deliberately NOT the same as .is-missing — see the note above. */
.cov-chip.is-deliberate{background:var(--nr-bg);color:var(--nr-tx)}
/* And not the same as .is-deliberate either: "no row" means the product is
 * not in this store's catalogue at all, which is a different fact from
 * "decided against". 595 codes are legitimately absent from some stores.
 *
 * --ink2 RATHER THAN THE --ink3 THIS SHIPPED WITH. Measured in a real
 * browser: --ink3 on --line2 is 4.39:1 in the DEFAULT theme, just under the
 * 4.5:1 floor, while passing in both of the others (5.04 and 5.50) — so the
 * one theme most people use was the one that failed. --ink2 is 6.83 / 9.10 /
 * 9.14. This is the quietest chip on the screen and the easiest to leave
 * unmeasured. */
.cov-chip.is-norow{background:var(--line2);color:var(--ink2)}

.cov-legend{display:flex;flex-wrap:wrap;gap:var(--sp-3);align-items:center}
.cov-legend-item{display:inline-flex;align-items:center;gap:6px;color:var(--ink2)}

/* ===== APPROVAL QUEUE TABLE
 * The owner, on the first table version: "the rows height are too thick".
 * They were, and the cause was not the padding — it was that four cells
 * each stacked two lines (code over name, requester over role, verb over
 * consequence) and the actions cell carried a 220px input with a top
 * margin meant for a card. A row was five lines tall, so twenty-five of
 * them scrolled like a hundred.
 *
 * Everything here is therefore about keeping ONE line per row: the second
 * line of each pair moves inline or into a title attribute, and what is
 * left is clipped rather than wrapped. Clipping is safe because none of
 * these cells is the only place its value appears — the code is beside
 * the name, and the full reason is in the cell's title. */
.aq td{padding-top:6px;padding-bottom:6px;white-space:nowrap}
/* Inline, not stacked: the code identifies, the name confirms. */
.aq .kode{margin-right:var(--sp-2)}
/* THE PRODUCT NAME IN FULL (owner, 2 Oct 2026: "auto extend the product name
 * to its maximum width"): one line, never clipped, so the column is as wide
 * as its longest name and .table-scroll scrolls sideways when it must. It
 * was a draggable width that every sort reset. */
.aq-name{white-space:nowrap}

/* ===== RULE REQUEST OUTCOMES — the 'recently decided' list
 *
 * A plain list rather than a second table: it is read as prose ("X asked,
 * Y decided, here is why") and a table would promise columns to scan that
 * nobody scans. It is the ONLY place the person who asked learns the answer,
 * because this app sends no mail, so it is deliberately roomy rather than
 * compact.
 */
.rr-decided{list-style:none;margin:0;padding:0;display:grid;gap:var(--sp-2)}
.rr-decided li{font-size:var(--fs-sm);color:var(--ink2);line-height:1.5}
.rr-decided .pill{margin-right:var(--sp-2)}

/* ===== USER STORE GRANTS — which shops one account covers
 *
 * A staff account holds SEVERAL shops since 18 Sep 2026, so the management
 * screen picks them with a fieldset of checkboxes rather than a <select>.
 * There are 47 shops and growing, so the list scrolls rather than pushing the
 * Save button off the panel; max-height is in `em` so it follows the type
 * scale instead of pinning a pixel count.
 *
 * The columns are `auto-fill` rather than a fixed count: a store code is a
 * known width, so the browser fits as many as the drawer allows and reflows
 * to one column on a narrow screen without a media query.
 */
.um-stores{border:0;padding:0;margin:0}
.um-stores legend{
  font-weight:600;font-size:var(--fs-sm);color:var(--ink2);
  padding:0;margin-bottom:var(--sp-2);
}
.um-storelist{
  display:grid;grid-template-columns:repeat(auto-fill,minmax(14ch,1fr));
  gap:var(--sp-1) var(--sp-3);
  max-height:14em;overflow-y:auto;
  border:1px solid var(--line);border-radius:var(--radius-sm);
  background:var(--control-bg);
  padding:var(--sp-2) var(--sp-3);
}
.um-storeopt{
  display:flex;align-items:center;gap:var(--sp-2);
  font-size:var(--fs-sm);white-space:nowrap;cursor:pointer;
}
.um-storeopt input{margin:0;flex:none}
.um-storeopt:hover{color:var(--ink)}

/* ===== DATA MANAGEMENT: how to register a missing job
 *
 * /data-management runs and re-times scheduled tasks and deliberately does
 * NOT create one (App\Services\Ops\TaskRegistration says why). A job the
 * scheduler does not have therefore shows the command that would create it,
 * closed behind a <details> so three missing jobs cannot bury the five that
 * are working.
 *
 * EVERY COLOUR IS A TOKEN. The block sits on the ordinary table surface, not
 * on a hero panel, so it needs no per-theme counterpart — which is exactly
 * the distinction HeroPanelThemeTest draws. */
.task-register{margin-top:var(--sp-2);font-size:var(--fs-xs)}
.task-register > summary{
  cursor:pointer;color:var(--accent);font-weight:600;
  /* The marker is the only affordance here, so it keeps its default
     disclosure triangle rather than being restyled into something that
     reads as a button. */
  width:fit-content;
}
.task-register > summary:hover{text-decoration:underline}
.task-register > p{margin:var(--sp-2) 0 0}
.task-register-cmd{
  margin:var(--sp-2) 0;
  padding:var(--sp-2) var(--sp-3);
  background:var(--inset);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
  /* A command must be copyable EXACTLY, so it wraps rather than scrolls
     sideways in a table cell — a horizontally clipped line is one a reader
     cannot check before they paste it. pre-wrap keeps the newlines, which
     are what make it four statements instead of one unrunnable smear. */
  white-space:pre-wrap;
  word-break:break-all;
  font-family:var(--font-mono);
  font-size:var(--fs-xs);
  line-height:1.5;
  color:var(--ink2);
  max-height:18em;overflow-y:auto;
}

/* ===== BLACKLIST MARKER — the one action with no pile of its own
 *
 * A count of blacklisted products, printed on the SECTION, CATEGORY and
 * BRAND roll-up rows of the store dashboard's drill-down.
 *
 * WHY IT IS INSIDE THE NAME CELL rather than a column of its own: .cat-crow
 * is a seven-column grid (18px 16px 1fr auto auto auto auto) and .catrow a
 * four-column one, both of them shared by rows that have no marker. Adding
 * an eighth cell would put an empty track on every row in the app to carry a
 * badge that appears on a handful. Inline after the name costs no grid
 * change and reads in the order you scan: what it is, then what is wrong
 * with it.
 *
 * It borrows --urgent-bg/--urgent-tx, the refusals-and-failures pair, which
 * is the same ground the product-level BLACKLIST pill uses (.p-urgent). The
 * marker and the pill it is summarising therefore cannot drift apart in a
 * theme, and both were measured by PaletteContrastTest as part of that pair.
 *
 * NOT .pill: that is padded for a status cell of its own and has
 * white-space:nowrap, which on a narrow name cell overflows the row rather
 * than wrapping. This is deliberately smaller and allowed to shrink.
 */
.bl-mark{
  display:inline-block;margin-left:var(--sp-2);padding:1px 7px;
  border-radius:20px;
  background:var(--urgent-bg);color:var(--urgent-tx);
  font-size:var(--fs-micro);font-weight:600;letter-spacing:.01em;
  vertical-align:middle;
}
/* The section header is larger type and upper-case; the marker should not
 * inherit either, or it reads as part of the section's own name. */
.catsec .bl-mark{text-transform:none;letter-spacing:0}

/* ===== LISTING RULES — the Map's search box and the page's busy note.
 * (The brand × scope policy table these lived beside was retired on
 * 1 Oct 2026, and the category table on 2 Oct.) */
.pt-search{width:220px}

.pt-busy{font-size:var(--fs-xs)}


.um-fold[x-cloak]{display:none}

/* ===== APPLIED RULES — the Map's shared pieces: a group's name and count,
 * the tag that says where a setting was set (Co / Div / Shop), and a
 * setting's block with the rules behind it. Tokens and existing pills only.
 */
.ar-gname{
  background:none;border:0;padding:0;font:inherit;font-weight:700;color:var(--ink);
  letter-spacing:.02em;cursor:pointer;display:inline-flex;align-items:center;gap:var(--sp-1);
}
span.ar-gname{cursor:default}
.ar-gsum{margin-left:var(--sp-3);font-weight:400;font-size:var(--fs-xs);color:var(--ink3)}

/* Where a setting was set, as quiet as a footnote: the value is what a
 * reader scans for, the tag only answers "whose is this". */
.ar-src{
  margin-left:var(--sp-1);padding:0 4px;border:1px solid var(--line);border-radius:var(--radius-xs);
  font-size:var(--fs-micro);color:var(--ink3);vertical-align:1px;
}
.ar-set{padding:var(--sp-3) 0;border-bottom:1px solid var(--line2)}
.ar-set:first-child{padding-top:0}
.ar-set h4{margin:0 0 var(--sp-1);font-size:var(--fs-xs);font-weight:600;color:var(--ink3);text-transform:uppercase;letter-spacing:.04em}
.ar-val{margin-left:var(--sp-2);font-size:var(--fs-sm);font-weight:650;color:var(--ink);text-transform:none;letter-spacing:0}
.ar-default{margin:0;font-size:var(--fs-xs);color:var(--ink3)}
.ar-decs{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--sp-1);font-size:var(--fs-xs)}
.ar-dec{display:flex;flex-wrap:wrap;gap:var(--sp-1) var(--sp-2);align-items:baseline}
.ar-dec-what{color:var(--ink3);min-width:6.5em}
.ar-dec-what:empty{display:none}
.ar-dec-rule{color:var(--ink2)}
.ar-shops{display:flex;flex-wrap:wrap;gap:var(--sp-1) var(--sp-2)}
/* Each shop's own rules in the Map details' right-hand column: the shop,
 * then one line per rule with who set it and Withdraw. */
.ar-shoprules,.ar-shoprules ul{list-style:none;margin:0;padding:0}
.ar-shoprules>li{padding:var(--sp-2) 0;border-top:1px solid var(--line2)}
.ar-shoprules>li:first-child{border-top:0;padding-top:0}
.ar-shoprules-shop{font-weight:700}
.ar-shoprules ul li{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 var(--sp-2);margin-top:var(--sp-1);font-size:var(--fs-xs)}
.ar-shoprule-says{color:var(--ink);overflow-wrap:anywhere}
.ar-shoprules .meta-line{display:inline;color:var(--ink3)}
.ar-shoprules-wider>li{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 var(--sp-2);font-size:var(--fs-xs)}
.ar-audit{margin-top:var(--sp-4)}
/* The Map / Rules switch beside the panel's heading. */
.ar-views{margin-left:var(--sp-4)}
/* The Map tab (1 Oct 2026): tiles across the full width, with the picked
 * one's details opening in place among them (MAP DETAILS below). A tile is sized by
 * products (flex-grow), tinted by outcome with a strong edge in the same
 * hue, and carries a symbol so colour is never the only signal. */
.ar-mapwrap{display:grid;grid-template-columns:minmax(0,1fr)}
.ar-map{padding:var(--sp-4) var(--sp-5);min-width:0}
.ar-maptop{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:var(--sp-2) var(--sp-4);margin-bottom:var(--sp-3)}
.ar-crumbs{display:flex;align-items:center;flex-wrap:wrap;gap:var(--sp-1);margin-bottom:var(--sp-2);font-size:var(--fs-xs);font-weight:650;color:var(--ink)}
.ar-map-hint{margin:0;font-size:var(--fs-xs);color:var(--ink3)}
.ar-tiles-sm .ar-tile{flex-basis:84px;min-height:44px}
.ar-gname.is-picked{text-decoration:underline;text-decoration-thickness:3px;text-decoration-color:var(--accent);text-underline-offset:4px}
.ar-crumbs .rowact{font-weight:650}
.ar-map-key{display:flex;flex-wrap:wrap;gap:var(--sp-1) var(--sp-4);margin:0;font-size:var(--fs-xs);color:var(--ink2)}
.ar-map-key span{display:inline-flex;align-items:center;gap:var(--sp-1)}
.ar-map-key i{font-style:normal}
.ar-map-key .ar-tile{flex:none;min-height:0;padding:0 6px;cursor:default;font-weight:700}
.ar-mapgroup{margin-bottom:var(--sp-4)}
.ar-mapgroup h3{margin:0 0 var(--sp-2);font-size:var(--fs-sm);font-weight:700;color:var(--ink);letter-spacing:.02em}
.ar-tiles{display:flex;flex-wrap:wrap;gap:5px}
.ar-tile{
  flex:1 1 104px;max-width:100%;min-height:56px;padding:var(--sp-1) var(--sp-2);
  border:0;border-left:5px solid transparent;border-radius:var(--radius-xs);
  font:inherit;text-align:left;cursor:pointer;
  display:flex;flex-direction:column;justify-content:space-between;gap:2px;overflow:hidden;
}
.ar-tile:hover{filter:saturate(1.25) brightness(.97)}
.ar-tile-name{font-size:var(--fs-xs);font-weight:700;line-height:1.2;overflow-wrap:anywhere}
.ar-tile-meta{font-size:var(--fs-micro);font-weight:600;display:flex;align-items:center;gap:var(--sp-1);flex-wrap:wrap}
.ar-tile-prim{background:var(--map-prim-bg);color:var(--map-prim-tx);border-left-color:var(--map-prim-edge)}
.ar-tile-sec{background:var(--map-sec-bg);color:var(--map-sec-tx);border-left-color:var(--map-sec-edge)}
.ar-tile-skip{background:var(--map-skip-bg);color:var(--map-skip-tx);border-left-color:var(--map-skip-edge)}
.ar-tile-black{background:var(--map-black-bg);color:var(--map-black-tx);border-left-color:var(--map-black-edge)}
/* Colour by Stock floor or Codes: a setting in force (indigo) or none (the
 * neutral skip tint). */
.ar-tile-on{background:var(--map-on-bg);color:var(--map-on-tx);border-left-color:var(--map-on-edge)}
.ar-tile-off{background:var(--map-skip-bg);color:var(--map-skip-tx);border-left-color:var(--map-skip-edge)}
.ar-colourby{align-items:center}
.ar-mapsearch .search{width:min(260px,100%)}
.ar-shopsetup{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2);margin:0 0 var(--sp-3);padding:var(--sp-2) var(--sp-3);background:var(--inset);border-radius:var(--radius-xs)}
.store-rules{padding:var(--sp-3) var(--sp-5)}
.sr-top{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:var(--sp-2);margin-bottom:var(--sp-2)}
/* What this level set itself: a dark ring and a badge saying whose. */
.ar-tile.is-own{box-shadow:inset 0 0 0 2px var(--ink)}
.ar-own,.ar-key-own{
  display:inline-block;padding:0 5px;border-radius:var(--radius-xs);
  font-size:var(--fs-micro);font-weight:700;font-style:normal;background:var(--ink);color:var(--panel);
}
.ar-tile.is-picked{outline:3px solid var(--accent);outline-offset:2px}
.ar-tile.is-ticked{outline:3px dashed var(--accent);outline-offset:2px}
.ar-tick{font-weight:400}
/* Product rows in the Map's panel. */
.ar-prow{display:flex;flex-direction:column;gap:var(--sp-1);padding:var(--sp-2) 0;border-bottom:1px solid var(--line2)}
.ar-prow-id{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--sp-1) var(--sp-2);font-size:var(--fs-xs)}
.ar-pname{color:var(--ink2);overflow-wrap:anywhere}
.ar-prow-floor{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2);font-size:var(--fs-xs)}
.ar-prow-floor .ar-inline{margin:0}
.ar-num-sm{width:4.5em}
/* ===== RULES FOR THIS SHOP (store dashboard, owner 1 Oct 2026)
 * Every listing rule reaching the shop, by where it was set, with "Ask to
 * withdraw" on the shop's own for staff. A heading that opens it. */
.shop-rules-toggle{border:0;background:transparent;padding:0;font:inherit;color:inherit;cursor:pointer;display:inline-flex;gap:var(--sp-2);align-items:center}
.shop-rules-toggle:focus-visible{box-shadow:var(--focus-ring);border-radius:4px}
.shop-rules-group+.shop-rules-group{margin-top:var(--sp-4)}
.shop-rules-group h3{margin:0 0 var(--sp-2);font-size:var(--fs-sm);font-weight:650;color:var(--ink2)}
.shop-rules-group ul{list-style:none;margin:0;padding:0}
.shop-rules-group li{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 var(--sp-3);padding:var(--sp-2) 0;border-top:1px solid var(--line2)}
.shop-rule-says{color:var(--ink);overflow-wrap:anywhere}
.shop-rules-group .meta-line{display:inline;color:var(--ink3)}

/* ===== STORE NAME TOOLTIP (owner, 1 Oct 2026: "i want the name to appear
 * immediately"). The shop's name over a store code, drawn by the layout's
 * script on pointer-over and keyboard focus. A title would wait about a
 * second; this shows at once. Fixed, so a scrolling table cannot clip it;
 * ink-on-panel inverted, which reads in every theme; never takes the
 * pointer, so it cannot sit between the reader and what they click. */
/* A column header that explains itself on hover or focus (the layout tooltip). */
.th-tip{cursor:help;border-bottom:1px dotted currentColor;border-radius:var(--radius-xs)}
.th-tip:focus-visible{box-shadow:var(--focus-ring);outline:none}
.store-tip{
  position:fixed;z-index:450;pointer-events:none;max-width:min(22rem,calc(100vw - 16px));
  padding:4px 8px;border-radius:6px;background:var(--ink);color:var(--panel);
  font-size:var(--fs-xs);font-weight:600;line-height:1.35;
  box-shadow:0 4px 14px rgba(var(--shadow-rgb),.25);
  /* Purchase's product tip is several lines (name, SOH, T7D...) joined by
   * newlines; a store name has none, so this changes nothing for it. */
  white-space:pre-line;
}

/* ===== MAP DETAILS (owner, 1 Oct 2026: "not a popped up center, but with
 * the tiles shifted down to make room for the expanded information")
 * The picked tile's details open IN the map: a full-width item among the
 * tiles of its group (flex-basis 100% breaks the row), placed after the last
 * tile of the pressed tile's row by the `order` its own script sets, so the
 * rows below move down. The shops with rules of their own sit in a column
 * on the right. No transform anywhere (the .content / .rdrawer trap). */
.ar-mapdetail{
  flex:1 0 100%;min-width:0;margin:var(--sp-2) 0 var(--sp-3);
  background:var(--panel);border:1px solid var(--line);border-top:3px solid var(--accent);border-radius:12px;
  box-shadow:0 8px 24px rgba(var(--shadow-rgb),.12);
  padding:var(--sp-4) var(--sp-5);scroll-margin:var(--sp-6);
}
.ar-mapdetail-head{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--sp-3) var(--sp-5);flex-wrap:wrap;padding-bottom:var(--sp-3);border-bottom:1px solid var(--line)}
.ar-mapdetail-title{min-width:0}
.ar-mapdetail-title h3{margin:0;font-size:var(--fs-md);font-weight:700;color:var(--ink)}
.ar-mapdetail-title h3:focus{outline:none}
.ar-mapdetail-title h3:focus-visible{box-shadow:var(--focus-ring);border-radius:4px}
/* Whose setting the details are, beside the title: a division's in the
 * accent's wash, ONE SHOP's in the solid accent, because a shop setting
 * outranks the division and is the one most easily mistaken for it. */
.ar-mapdetail-titlerow{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-1) var(--sp-3)}
.ar-scopebadge{display:inline-flex;align-items:center;gap:var(--sp-1);padding:3px 10px;border-radius:999px;font-size:var(--fs-xs);font-weight:600;
  background:var(--accent-soft);color:var(--accent);border:1px solid var(--accent-line)}
.ar-scopebadge.is-shop{background:var(--accent);color:var(--on-accent);border-color:var(--accent)}
.ar-mapdetail-tools{display:flex;align-items:center;gap:var(--sp-2);flex-shrink:0}
.ar-mapdetail-body{display:grid;grid-template-columns:minmax(0,1fr) minmax(220px,280px);gap:0 var(--sp-6)}
.ar-mapdetail-body:not(:has(.ar-mapdetail-side)){grid-template-columns:minmax(0,1fr)}
.ar-mapdetail-main{min-width:0}
.ar-mapdetail-side{border-left:1px solid var(--line);padding-left:var(--sp-5);min-width:0}
/* All three settings in one row, so every one of them is in view at once;
 * they stack when there is no room for three. */
.ar-mapdetail-sets{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:0 var(--sp-6);border-bottom:1px solid var(--line)}
.ar-mapdetail .ar-set{padding:var(--sp-3) 0}
.ar-mapdetail .tier3{margin:var(--sp-1) 0 var(--sp-2)}
/* A product is one line here: what it is, its three-way control, its
 * floor. .cat-state's `flex:0 0 232px` is a WIDTH in the boards' rows; in a
 * column it became a 232px-tall gap under a locked product, so the row is a
 * grid and the basis is switched off. */
.ar-mapdetail .ar-prow{display:grid;grid-template-columns:minmax(0,1fr) 232px minmax(0,auto);align-items:center;gap:var(--sp-1) var(--sp-4)}
.ar-prow .cat-state{flex:none}
.ar-step{
  width:32px;height:32px;display:grid;place-items:center;padding:0;
  border:1px solid var(--line);border-radius:50%;background:var(--panel);color:var(--ink);
  font:inherit;font-size:var(--fs-lg);line-height:1;cursor:pointer;
}
.ar-step:hover{background:var(--row-hover)}
.ar-step:focus-visible{box-shadow:var(--focus-ring)}
@media(max-width:1100px){
  .ar-mapdetail-body{grid-template-columns:minmax(0,1fr)}
  .ar-mapdetail-side{border-left:0;padding-left:0;border-top:1px solid var(--line)}
  .ar-mapdetail .ar-prow{grid-template-columns:minmax(0,1fr)}
}
.ar-panel-note{margin:0 0 var(--sp-2)}
.ar-panel-foot{margin:var(--sp-3) 0 0}
.ar-inline{display:flex;align-items:center;gap:var(--sp-2);margin:var(--sp-1) 0 var(--sp-2);flex-wrap:wrap}
.ar-num{width:6em}
.ar-codes{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-1);margin:var(--sp-1) 0 var(--sp-2)}
.ar-code{
  padding:2px var(--sp-2);border:1px solid var(--line);border-radius:999px;
  font:inherit;font-size:var(--fs-xs);font-weight:650;background:var(--map-skip-bg);color:var(--map-skip-tx);
  text-decoration:line-through;
}
button.ar-code{cursor:pointer}
.ar-code.on{background:var(--map-prim-bg);color:var(--map-prim-tx);border-color:var(--map-prim-edge);text-decoration:none}


/* ===== PRINT — a dashboard on paper (24 Sep 2026)
 *
 * Nothing here was written for paper, so a printed dashboard carried the
 * navy rail, the topbar's pills and the dark hero panels (a page of solid
 * ink), and a long table was cut off at its scroll box. This keeps what a
 * printout is for — the figures and the tables — and drops what only
 * works on a screen: navigation, pills that poll, buttons, floating bars.
 *
 * The hero panels need no rules of their own: they read --hero-* tokens,
 * so re-pointing those to paper and ink is the whole change. */
@media print{
  /* `html:root` so this out-ranks the :root[data-theme=…] blocks further
   * down: paper is white whichever theme the person reads on screen, and a
   * dark-theme printout would otherwise be a page of near-black. */
  html:root,html:root[data-theme]{
    --bg:#fff;  --panel:#fff;  --line:#ccc;  --line2:#eee;
    --ink:#000; --ink2:#333;   --ink3:#555;
    --hero-bg:#fff;       --hero-edge:#999;
    --hero-ink:#000;      --hero-good:#000;
    --hero-label:#333;    --hero-num-alt:#333;
    --hero-faint:#555;    --hero-line:#bbb;
  }
  body{background:#fff}
  /* Not `button` wholesale: a sortable column header IS a button, and
   * hiding it would print every table without its headings. */
  .rail,.skip-link,.topbar-right,.content-alert,.scope-strip,
  .bulkbar,.bulkbar-spacer,.rmenu,.rdrawer,.rdrawer-scrim,
  .pager,.rmenu-btn,.btn,.btn-sm,.nav-toggle,.rail-scrim{display:none !important}
  .shell{display:block}
  .content{padding:0;overflow:visible}
  .panel,.card{box-shadow:none;break-inside:avoid-page}
  /* A scroll box on paper is a cut-off table: let every row print. */
  .table-scroll,.table-tall{overflow:visible;max-height:none}
  .table-tall thead th,.pin-actions th:last-child,.pin-actions td:last-child{position:static}
  thead{display:table-header-group}   /* the header repeats on every page */
  tr{break-inside:avoid}
  /* Pills and bars carry meaning in their fill; browsers drop background
   * colours when printing unless told otherwise. */
  .pill,.cov-chip,.pilebar,.bar,.plat-badge,.scope-tag{-webkit-print-color-adjust:exact;print-color-adjust:exact}
  a{color:inherit;text-decoration:none}
}

/* ===== DATA CURRENT — /data-management's "How current is the data" (25 Sep 2026)
 * Its own panel above the jobs table, so it needs the gap .panel does not
 * carry. The per-shop table sits inside a closed <details> and should be as
 * wide as its three short columns, not the whole cell. */
.data-current{margin-bottom:var(--sp-5)}
.data-current-shops{margin-top:var(--sp-2)}
.data-current-shops table{width:auto}
/* The store dashboard's one line on when its stock was last checked. */
.stock-checked{font-size:var(--fs-xs);margin:0 0 var(--sp-3)}

/* ===== THE AGRES LOOK (design prototype, 26 Sep 2026)
 *
 * Owner: "it feels so dull and uninspiring" — and a brand to follow,
 * agreskomputer.com. Everything here is additive: the brand mark, the
 * brand band, the completion ring, sparklines, the podium, and motion that
 * means something (a figure counting to its value, a bar growing to its
 * length, a celebration when a store is finished). All motion is off under
 * prefers-reduced-motion, at the bottom of this section. */

/* The rail: the brand band across its top edge, the swirl, and the active
 * row in the brand fill. */
.rail::before{content:"";position:absolute;left:0;right:0;top:0;height:3px;background:var(--brand-grad)}
.brand-mark{width:34px;height:34px;background:none;border-radius:0}
.brand-mark .agres-mark{width:34px;height:34px;display:block}
.agres-mark .m-a{fill:var(--brand-mark)}
.agres-mark .m-b{fill:var(--brand-mark-deep)}
.rail .brand-name{font-weight:800;letter-spacing:.02em}
.nav a{transition:background-color .15s ease,color .15s ease}
.nav a.on{background:var(--brand-fill);box-shadow:0 6px 16px var(--brand-glow)}

/* The topbar: a greeting over a heavier title, and the brand band as its
 * bottom edge instead of a grey hairline. */
.topbar{position:relative;border-bottom-color:transparent}
.topbar::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--brand-grad);opacity:.85}
.greet{font-size:var(--fs-xs);font-weight:600;color:var(--ink3);margin-bottom:2px}
.topbar h1{font-weight:800;letter-spacing:-.5px;line-height:1.15}

/* Primary actions wear the brand fill — SOLID buttons only: a ghost button
 * keeps its paper ground and accent ink, and a disabled one its grey. */
.btn:not(.btn-ghost):not(:disabled),.btn-sm:not(.btn-ghost):not(.cl-sel-remove):not(:disabled){background:var(--brand-fill);transition:filter .15s ease,box-shadow .15s ease}
.btn:not(.btn-ghost):not(:disabled):hover,.btn-sm:not(.btn-ghost):not(.cl-sel-remove):not(:disabled):hover{filter:brightness(1.06);box-shadow:0 4px 12px var(--brand-glow)}
.div-btn.on{background:var(--brand-fill);border-color:transparent}

/* Headings a step heavier — the face carries weight well. */
.panel-head h2{font-weight:750;letter-spacing:-.2px}

/* ---- the hero: a ring beside the caption --------------------------------- */
.hero-ringed{display:flex;align-items:center;gap:var(--sp-6);flex-wrap:nowrap}
.hero-ringed .hero-main{flex:1;min-width:0}
.hero-ringed .hero-cap{max-width:48em}
.hero-kicker{font-size:var(--fs-micro);font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--hero-faint);margin-bottom:var(--sp-1)}
.hero-lede{font-size:var(--fs-md);color:var(--hero-label);max-width:36em;line-height:1.45}
.hero-lede b{color:var(--hero-ink)}

.ring{position:relative;width:var(--ring-size);height:var(--ring-size);flex-shrink:0}
.ring svg{width:100%;height:100%;transform:rotate(-90deg);display:block}
.ring-track{fill:none;stroke:var(--line2);stroke-width:3.4}
.ring-arc{fill:none;stroke:url(#ringGrad);stroke-width:3.4;stroke-linecap:round;animation:ring-in 1.1s cubic-bezier(.2,.8,.2,1) both}
@keyframes ring-in{from{stroke-dasharray:0 100}}
.ring-label{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-weight:800;color:var(--hero-ink);letter-spacing:-.04em;font-size:calc(var(--ring-size) * .27);font-variant-numeric:proportional-nums}
.ring-pc{font-size:.5em;color:var(--hero-faint);margin-left:1px;align-self:center;margin-top:.25em}

/* ---- the tiles: lift under the pointer, and a sparkline ----------------- */
.acttile{transition:transform .18s ease,box-shadow .18s ease,background-color .18s ease}
.acttile:hover{transform:translateY(-2px);box-shadow:0 10px 24px rgba(var(--shadow-rgb),.10)}
.at-num{font-size:var(--fs-xl);letter-spacing:-.6px}
.spark{display:block;width:100%;height:28px;margin-top:var(--sp-2);color:var(--tile-dot);overflow:visible}
.spark-area{fill:currentColor;opacity:.14}
.spark-line{fill:none;stroke:currentColor;stroke-width:1.75;stroke-linejoin:round;stroke-linecap:round}

/* ---- the podium ----------------------------------------------------------- */
.podium-head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3);margin:var(--sp-2) 0 var(--sp-3)}
.podium-head h2{font-size:var(--fs-md);font-weight:750}
.podium{list-style:none;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp-3);margin:0 0 var(--sp-5)}
.podium-card a{
  display:grid;grid-template-columns:auto 1fr auto;grid-template-areas:"medal store pct" "bar bar bar" "sub sub sub";
  align-items:center;column-gap:var(--sp-3);row-gap:var(--sp-2);
  background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:var(--sp-4) var(--sp-5);
  box-shadow:var(--elev);text-decoration:none;color:var(--ink);
  transition:transform .18s ease,box-shadow .18s ease;position:relative;overflow:hidden;
}
.podium-card a:hover{transform:translateY(-2px);box-shadow:0 10px 24px rgba(var(--shadow-rgb),.10)}
.podium-card.rank-1 a::before{content:"";position:absolute;left:0;right:0;top:0;height:3px;background:var(--brand-grad)}
/* THE PODIUM EARNS ITS PLACE (owner, 26 Sep 2026: "can have special effects
 * to grab attention"). Each card takes its medal's metal as a wash from the
 * top-left corner, its edge and its glow; the leader gets a crown, and a
 * light sweeps its card once when the page arrives and again under the
 * pointer — never on a loop, and not at all under reduced motion. The metals
 * are tokens (--podium-*), so Colourblind turns them to greys. */
.podium-card a{border-color:var(--podium-line);background:var(--podium-bg);box-shadow:0 6px 18px var(--podium-glow)}
.podium-card.rank-1{--podium-bg:var(--podium-1-bg);--podium-line:var(--podium-1-line);--podium-glow:var(--podium-1-glow)}
.podium-card.rank-2{--podium-bg:var(--podium-2-bg);--podium-line:var(--podium-2-line);--podium-glow:var(--podium-2-glow)}
.podium-card.rank-3{--podium-bg:var(--podium-3-bg);--podium-line:var(--podium-3-line);--podium-glow:var(--podium-3-glow)}
.podium-card a:hover{box-shadow:0 12px 28px var(--podium-glow)}
.podium-card a::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(105deg,transparent 30%,var(--sheen) 50%,transparent 70%);
  transform:translateX(-100%);animation:podium-sheen 1.6s ease-out .5s 1 both;
}
.podium-card.rank-2 a::after{animation-delay:.75s}
.podium-card.rank-3 a::after{animation-delay:1s}
.podium-card a:hover::after{animation-name:podium-sheen-again;animation-delay:0s}
@keyframes podium-sheen{from{transform:translateX(-100%)}to{transform:translateX(100%)}}
@keyframes podium-sheen-again{from{transform:translateX(-100%)}to{transform:translateX(100%)}}
.podium-card.rank-1 .medal{animation:medal-pop .6s cubic-bezier(.3,1.6,.5,1) .3s both}
@keyframes medal-pop{from{transform:scale(.4);opacity:0}}
.podium-crown{
  grid-area:store;justify-self:end;align-self:start;margin-right:-4px;
  display:inline-flex;align-items:center;gap:4px;font-size:var(--fs-micro);font-weight:800;
  color:var(--podium-1-tx);background:var(--podium-1-chip);padding:2px 8px;border-radius:999px;
}
.podium-crown svg{width:12px;height:12px}

/* The mover cards: the riser green, the faller red — a wash from the corner,
 * the tone's edge and glow, a glyph in a disc, and the same one-off sweep. */
.cards .mover{position:relative;overflow:hidden;padding-left:72px;transition:box-shadow .18s ease}
.mover-up{background:linear-gradient(135deg,var(--ok-bg) 0%,var(--panel) 62%);border-color:var(--ok-line);box-shadow:0 6px 18px color-mix(in srgb,var(--ok-dot) 20%,transparent)}
.mover-down{background:linear-gradient(135deg,var(--stock-bg) 0%,var(--panel) 62%);border-color:var(--urgent-line);box-shadow:0 6px 18px color-mix(in srgb,var(--stock-dot) 18%,transparent)}
.mover-ico{position:absolute;left:20px;top:50%;transform:translateY(-50%);width:38px;height:38px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:var(--panel)}
.mover-ico svg{width:20px;height:20px}
.mover-up .mover-ico{color:var(--ok-tx);box-shadow:0 0 0 4px color-mix(in srgb,var(--ok-dot) 22%,transparent)}
.mover-down .mover-ico{color:var(--stock-tx);box-shadow:0 0 0 4px color-mix(in srgb,var(--stock-dot) 22%,transparent)}
.cards .mover::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(105deg,transparent 30%,var(--sheen) 50%,transparent 70%);
  transform:translateX(-100%);animation:podium-sheen 1.6s ease-out 1.2s 1 both;
}
.cards .mover-down::after{animation-delay:1.4s}
.cards .mover:hover::after{animation-name:podium-sheen-again;animation-delay:0s}
@media (prefers-reduced-motion:reduce){ .cards .mover::after{animation:none} }
.podium-card.rank-1 .podium-store{grid-area:store}
@media (prefers-reduced-motion:reduce){
  .podium-card a::after,.podium-card.rank-1 .medal{animation:none}
}
.medal{grid-area:medal;width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:var(--fs-md);box-shadow:inset 0 -2px 0 rgba(var(--shadow-rgb),.12)}
.rank-1 .medal{background:var(--medal-1);color:var(--medal-1-tx)}
.rank-2 .medal{background:var(--medal-2);color:var(--medal-2-tx)}
.rank-3 .medal{background:var(--medal-3);color:var(--medal-3-tx)}
.podium-store{grid-area:store;font-family:var(--font-mono);font-weight:700;font-size:var(--fs-base)}
.podium-pct{grid-area:pct;font-size:26px;font-weight:800;letter-spacing:-.6px;color:var(--ok-tx)}
.podium-bar{grid-area:bar;height:8px;border-radius:4px;background:var(--line2);overflow:hidden}
.podium-bar span{display:block;height:100%;border-radius:4px;background:linear-gradient(90deg,var(--bar-green-a),var(--bar-green-b));animation:grow-x .9s cubic-bezier(.2,.8,.2,1) both;transform-origin:left}
.podium-sub{grid-area:sub;font-size:var(--fs-xs);color:var(--ink3)}
@media(max-width:900px){ .podium{grid-template-columns:1fr} }

/* ---- bars grow to their length, once ------------------------------------ */
@keyframes grow-x{from{transform:scaleX(0)}}
.pilebar .seg{transform-origin:left;animation:grow-x .9s cubic-bezier(.2,.8,.2,1) both}
.catbar .fill{transform-origin:left;animation:grow-x .8s cubic-bezier(.2,.8,.2,1) both}

/* ---- the celebration ----------------------------------------------------- */
.confetti{position:fixed;inset:0;pointer-events:none;z-index:300;overflow:hidden}
.confetti i{position:absolute;top:-14px;width:8px;height:13px;border-radius:2px;animation-name:confetti-fall;animation-timing-function:cubic-bezier(.25,.6,.4,1);animation-fill-mode:forwards}
.confetti .c0{background:var(--brand-a)} .confetti .c1{background:var(--brand-b)} .confetti .c2{background:var(--brand-c)}
.confetti .c3{background:var(--ok-dot)} .confetti .c4{background:var(--price-dot)}
@keyframes confetti-fall{to{transform:translate(var(--drift),110vh) rotate(var(--spin));opacity:.9}}

@media (prefers-reduced-motion:reduce){
  .ring-arc,.pilebar .seg,.catbar .fill,.podium-bar span{animation:none}
  .acttile,.podium-card a{transition:none}
  .acttile:hover,.podium-card a:hover{transform:none}
}

/* ---- Primary / Secondary / Skip as a pill-shaped segmented control -------
 * The segments sit in a grey track; the chosen one is lifted out of it on
 * its own tint, with its ring and tick (see .tier3-btn.on above) — so "which
 * one is on" reads as a raised button rather than a squint at a pale fill. */
.tier3{background:var(--line2);border:1px solid var(--line);border-radius:999px;padding:2px;gap:2px;overflow:visible}
.tier3-btn{border:0;border-right:0;border-radius:999px;background:transparent;padding:3px 12px;transition:background-color .15s ease,box-shadow .15s ease}
.tier3-btn:not(.on):hover{background:var(--panel)}
.tier3-btn.on{box-shadow:inset 0 0 0 1.5px currentColor,0 1px 3px rgba(var(--shadow-rgb),.14)}
/* SAME AS ITS PARENT (27 Sep 2026): a thin ring and no fill, so a skipped
 * group's hundred identical children recede and the rows that DIFFER keep the
 * full fill. The tick stays, so the setting is still marked by more than a
 * tint, and the title and label say which parent it matches. */
.tier3.echo .tier3-btn.on{background:transparent;box-shadow:inset 0 0 0 1px currentColor;opacity:.72}

/* ---- people -------------------------------------------------------------- */
.avatar{
  display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;
  width:var(--av-size);height:var(--av-size);border-radius:50%;
  font-size:calc(var(--av-size) * .4);font-weight:750;letter-spacing:.02em;color:#fff;line-height:1;
}
.av-1{background:var(--av-1)} .av-2{background:var(--av-2)} .av-3{background:var(--av-3)}
.av-4{background:var(--av-4)} .av-5{background:var(--av-5)}
.with-av{display:inline-flex;align-items:center;gap:var(--sp-2)}
.cl-who{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap}
.who-row{display:flex;align-items:center;gap:10px}
.who-text{min-width:0}
.who-row .avatar{box-shadow:0 0 0 2px rgba(255,255,255,.12)}
/* The account row is the Settings link (rail regrouped, 10 Oct 2026): it
 * used to be a "You" heading holding one link. Same hover as a nav link. */
a.who-row{color:inherit;text-decoration:none;border-radius:var(--radius-sm);padding:6px;margin:-6px;transition:background-color .15s ease}
a.who-row:hover{background:rgba(255,255,255,.06)}
a.who-row.on{background:rgba(255,255,255,.1)}
.who-gear{width:16px;height:16px;flex-shrink:0;margin-left:auto;color:var(--sidebar-ink2)}
a.who-row:hover .who-gear,a.who-row.on .who-gear{color:#fff}
/* A little air between headings, so the groups read as groups. */
.nav-sec + .nav-sec{margin-top:var(--sp-2)}
/* The menu scrolls on a short screen; its scrollbar belongs to the rail. */
.rail .nav{scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.18) transparent}

/* ---- the worklist's action tabs, and table heads -------------------------- */
.subtab{border-radius:999px;transition:background-color .15s ease,border-color .15s ease,color .15s ease}
.subtab.on{background:var(--brand-fill);border-color:transparent;box-shadow:0 4px 12px var(--brand-glow)}
thead th{color:var(--ink3);letter-spacing:.06em}

/* ---- the busy bar: a brand-coloured sweep along the top while a request
 * is in flight (script in layouts/app.blade.php). Fixed, so it costs the
 * layout nothing; over the content column, right of the rail. */
.busy-bar{position:fixed;top:0;left:0;right:0;height:3px;z-index:400;pointer-events:none;opacity:0;transition:opacity .2s ease;overflow:hidden}
.busy-bar::before{content:"";position:absolute;top:0;bottom:0;left:-40%;width:40%;background:var(--brand-grad);border-radius:4px}
.busy-bar.is-on{opacity:1}
.busy-bar.is-on::before{animation:busy-sweep 1.1s ease-in-out infinite}
@keyframes busy-sweep{to{left:100%}}

/* ---- an empty pile that is genuinely clear ------------------------------ */
.empty-clear{display:flex;flex-direction:column;align-items:center;gap:var(--sp-2);padding:var(--sp-10) var(--sp-5)}
.clear-mark{width:56px;height:56px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--ok-tx);background:var(--ok-bg);box-shadow:0 0 0 6px color-mix(in srgb,var(--ok-bg) 45%,transparent)}
.clear-mark svg{width:28px;height:28px}
.clear-title{font-size:var(--fs-lg);font-weight:800;color:var(--ink);letter-spacing:-.3px}

.is-finished b{color:var(--ok-tx)}

/* ---- the theme cards on /settings carry a miniature of each theme --------
 * *** LITERAL COLOURS, ON PURPOSE. *** Every other rule in this file reads a
 * token so it follows the chosen theme; these must NOT — the Dark card has
 * to look dark while you are still in Default. So each miniature spells out
 * its own theme's rail, paper, accent and action hues. Decoration only
 * (aria-hidden); the card's words say what the theme is. */
.theme-prev{display:flex;width:100%;height:74px;border-radius:8px;overflow:hidden;margin-bottom:var(--sp-2);border:1px solid var(--line)}
.theme-prev i{display:block}
.tp-rail{width:22%;padding:8px 5px;display:flex;flex-direction:column;gap:4px}
.tp-rail i{height:5px;border-radius:4px}
.tp-main{flex:1;padding:6px 7px;display:flex;flex-direction:column;gap:5px}
.tp-head{height:6px;width:45%;border-radius:3px}
.tp-card{flex:1;border-radius:var(--radius-xs);display:flex;align-items:center;gap:6px;padding:0 6px}
.tp-ring{width:20px;height:20px;border-radius:50%;border:3px solid}
.tp-lines{flex:1;display:flex;flex-direction:column;gap:4px}
.tp-lines i{height:4px;border-radius:2px}
.tp-lines i:last-child{width:60%}
.tp-pills{display:flex;gap:4px}
.tp-pills i{height:7px;flex:1;border-radius:4px}
/* Default */
.tp-default{background:#f5f7fa}
.tp-default .tp-rail{background:#0e1322}
.tp-default .tp-rail i{background:#3a4460} .tp-default .tp-rail i:first-child{background:linear-gradient(90deg,#c8123f,#7e22ce)}
.tp-default .tp-head{background:#171f2e}
.tp-default .tp-card{background:#fff}
.tp-default .tp-ring{border-color:#e11d48 #9333ea #22d3ee #22d3ee}
.tp-default .tp-lines i{background:#dfe5ee}
.tp-default .tp-pills i:nth-child(1){background:#6d94f7} .tp-default .tp-pills i:nth-child(2){background:#a67ddb}
.tp-default .tp-pills i:nth-child(3){background:#e2634a} .tp-default .tp-pills i:nth-child(4){background:#43ad68}
/* Colourblind */
.tp-colorblind{background:#f5f5f5}
.tp-colorblind .tp-rail{background:#fafafa;border-right:1px solid #e5e5e5}
.tp-colorblind .tp-rail i{background:#d4d4d4} .tp-colorblind .tp-rail i:first-child{background:#171717}
.tp-colorblind .tp-head{background:#171717}
.tp-colorblind .tp-card{background:#fff}
.tp-colorblind .tp-ring{border-color:#171717 #525252 #a3a3a3 #a3a3a3}
.tp-colorblind .tp-lines i{background:#e5e5e5}
.tp-colorblind .tp-pills i{background:#d4d4d4} .tp-colorblind .tp-pills i:nth-child(3){background:#737373}
/* Dark */
.tp-dark{background:#101010}
.tp-dark .tp-rail{background:#1d1a18}
.tp-dark .tp-rail i{background:#3d3a39} .tp-dark .tp-rail i:first-child{background:#ee6018}
.tp-dark .tp-head{background:#f2f0ed}
.tp-dark .tp-card{background:#1d1a18}
.tp-dark .tp-ring{border-color:#e11d48 #9333ea #22d3ee #22d3ee}
.tp-dark .tp-lines i{background:#3d3a39}
.tp-dark .tp-pills i:nth-child(1){background:#7aa2ff} .tp-dark .tp-pills i:nth-child(2){background:#b995ee}
.tp-dark .tp-pills i:nth-child(3){background:#f08a6c} .tp-dark .tp-pills i:nth-child(4){background:#a0ca92}

/* ---- the sign-in card wears the brand band on its top edge, as the app's
 * topbar does on its bottom one. */
.login-card{position:relative;overflow:hidden}
.login-card::before{content:"";position:absolute;left:0;right:0;top:0;height:4px;background:var(--brand-grad)}
.login-card a{color:var(--accent);font-weight:600}

/* ---- Data management: a glyph per pipeline step, a live dot per running job */
.step-name{display:inline-flex;align-items:center;gap:var(--sp-2)}
.step-state{width:20px;height:20px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:var(--fs-micro);font-weight:800;flex-shrink:0}
.st-ok{background:var(--ok-bg);color:var(--ok-tx)}
.st-degraded{background:var(--hold-bg);color:var(--hold-tx)}
.st-overdue,.st-failed{background:var(--urgent-bg);color:var(--urgent-tx)}
.st-unknown{background:var(--nr-bg);color:var(--nr-tx)}
.job-state{display:flex;align-items:center;gap:var(--sp-2)}
.job-state.is-running{color:var(--ok-tx);font-weight:650}
.job-state.is-failed{color:var(--urgent-tx);font-weight:600}
.live-dot{width:9px;height:9px;border-radius:50%;background:var(--ok-dot);flex-shrink:0;box-shadow:0 0 0 0 color-mix(in srgb,var(--ok-dot) 60%,transparent);animation:live-pulse 1.6s ease-out infinite}
@keyframes live-pulse{70%{box-shadow:0 0 0 7px transparent}100%{box-shadow:0 0 0 0 transparent}}
@media (prefers-reduced-motion:reduce){ .live-dot{animation:none} }

/* ---- the requests callout at the top of the dashboards ------------------ */
.alert-callout{display:flex;align-items:center;flex-wrap:wrap;gap:var(--sp-2) var(--sp-5);padding:var(--sp-3) var(--sp-4);border-radius:var(--radius)}
.callout-ico{width:32px;height:32px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--panel);color:var(--input-tx);flex-shrink:0;box-shadow:var(--elev)}
.callout-ico svg{width:17px;height:17px}
.callout-item{display:inline-flex;align-items:center;flex-wrap:wrap;gap:var(--sp-3)}
.callout-item b{font-size:var(--fs-md)}
.alert-pending a.callout-cta{
  display:inline-flex;align-items:center;min-height:32px;padding:0 var(--sp-3);border-radius:999px;
  background:var(--panel);border:1px solid var(--input-line);text-decoration:none;font-weight:700;font-size:var(--fs-xs);
  transition:background-color .15s ease,box-shadow .15s ease;
}
.alert-pending a.callout-cta:hover{box-shadow:0 4px 12px rgba(var(--shadow-rgb),.10)}
.alert-pending a.callout-cta::after{content:"\2192" / "";margin-left:6px}

/* ---- Product coverage (design prototype, 26 Sep 2026) ----------------------
 * Before a check the paste box is the page; after one it moves into a left
 * column and the answer takes the width. Below 1100px it stacks again. */
.cov-page.is-checked{display:grid;grid-template-columns:minmax(260px,320px) minmax(0,1fr);gap:var(--sp-5);align-items:start}
/* Before a check: the paste box and a "What you will see" key side by side,
 * so the page is not two thirds empty (27 Sep 2026). */
.cov-page:not(.is-checked){display:grid;grid-template-columns:minmax(0,3fr) minmax(0,2fr);gap:var(--sp-5);align-items:start}
.cov-preview-lead{margin:0 0 var(--sp-3);color:var(--ink2)}
.cov-preview-key{list-style:none;margin:0 0 var(--sp-3);padding:0;display:grid;gap:var(--sp-2)}
.cov-preview-key li{display:flex;align-items:center;gap:var(--sp-2);font-size:var(--fs-sm)}
.cov-page.is-checked .cov-input{position:sticky;top:var(--sp-4)}
/* The step list sits straight inside the panel here, and .panel clips its
 * overflow — so with no inner padding the numbered circles were cut off at
 * the left, the paste box's border and focus ring at the right, and the
 * Check button sat on the footer's rule. The padding gives all three room.
 * (Before the redesign too; the owner found it, 26 Sep 2026.) */
.cov-input .steps{padding:var(--sp-5) var(--sp-5) var(--sp-5) var(--sp-4)}
.cov-page.is-checked .cov-input .fl-paste{min-height:0}
.cov-input > .foot-note{display:block}
/* After a check the input column is still a live tool — paste again, press
 * Check again — so its steps are not dimmed as "done": at .55 opacity the
 * Check button read as disabled when it was not. */
.cov-page.is-checked .steps > li.is-waiting{opacity:1}
.cov-results > .panel + .panel,.cov-results > .panel{margin-bottom:var(--sp-4)}
@media(max-width:1100px){
  .cov-page.is-checked,.cov-page:not(.is-checked){display:block}
  .cov-page.is-checked .cov-input{position:static;margin-bottom:var(--sp-4)}
}

.cov-summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--sp-3);margin-bottom:var(--sp-4)}
.cov-stat{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:var(--sp-4) var(--sp-5);box-shadow:var(--elev);display:flex;flex-direction:column;gap:2px;border-top:3px solid var(--line)}
.cov-stat.is-good{border-top-color:var(--ok-dot)}
.cov-stat.is-bad{border-top-color:var(--stock-dot)}
.cov-stat-k{font-size:var(--fs-xs);font-weight:600;color:var(--ink3)}
.cov-stat-v{font-size:var(--fs-xl);font-weight:800;letter-spacing:-.6px;color:var(--ink);line-height:1.15}
.cov-stat.is-good .cov-stat-v{color:var(--ok-tx)}
.cov-stat.is-bad .cov-stat-v{color:var(--urgent-tx)}
.cov-stat-d{font-size:var(--fs-micro);color:var(--ink3)}
@media(max-width:900px){ .cov-summary{grid-template-columns:repeat(2,minmax(0,1fr))} }

.cov-filter .subtab{padding:4px var(--sp-3)}
.cov-legend{row-gap:var(--sp-2)}
.cov-legend-note{flex-basis:100%;color:var(--ink3)}

/* One row per code, with a coloured edge carrying its verdict. */
.cov-row{padding:var(--sp-2) var(--sp-3) var(--sp-2) 0;box-shadow:inset 3px 0 0 transparent}
.cov-row.cov-gaps{box-shadow:inset 3px 0 0 var(--stock-dot)}
.cov-row.cov-complete{box-shadow:inset 3px 0 0 var(--ok-dot)}
.cov-ident{display:flex;flex-direction:column;min-width:0;flex:1;gap:1px}
.cov-ident .cov-name{font-size:var(--fs-sm)}
.cov-badge{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border-radius:999px;font-size:var(--fs-xs);font-weight:750;white-space:nowrap;flex-shrink:0}
.cov-badge.is-complete{background:var(--ok-bg);color:var(--ok-tx)}
.cov-badge.is-gaps{background:var(--urgent-bg);color:var(--urgent-tx)}
.cov-badge.is-none{background:var(--nr-bg);color:var(--nr-tx)}
.cov-badge.is-unknown{background:var(--line2);color:var(--ink2);border:1px dashed var(--line)}
.cov-bar .catbar-wrap{width:200px}
/* The bar's parts are SPANS inside a button, and a span is inline: without
 * this the fill had no height and every coverage bar drew as an empty track
 * (it did before the redesign too). */
.cov-bar .catbar,.cov-bar .fill{display:block}

/* The code's header as two lines: verdict, code and name on the first; the
 * bar and the sentence on the second. One crowded line had the name cut to
 * nothing beside the bar on every product with a real description. */
.cov-head{display:grid;grid-template-columns:14px auto minmax(0,1fr);grid-template-areas:"tw badge ident" ". meter meter";align-items:center;column-gap:var(--sp-3);row-gap:6px}
.cov-head > .cov-twisty{grid-area:tw}
.cov-head > .cov-badge{grid-area:badge;justify-self:start}
.cov-head > .cov-ident{grid-area:ident;flex-direction:row;align-items:baseline;gap:var(--sp-2)}
.cov-head > .cov-meter{grid-area:meter;display:flex;align-items:center;flex-wrap:wrap;gap:var(--sp-2) var(--sp-4)}
.cov-head .cov-kode{font-size:var(--fs-sm)}
.cov-head .cov-name{font-size:var(--fs-base);color:var(--ink)}
.cov-extra:empty{display:none}

.cov-div-name{color:var(--ink2);font-weight:700}
.cov-div-head .muted{text-transform:none;letter-spacing:0;font-weight:500}
.cov-div-missing{text-transform:none;letter-spacing:0;font-weight:700;color:var(--urgent-tx);background:var(--urgent-bg);padding:1px 8px;border-radius:999px}
.cov-div-done{text-transform:none;letter-spacing:0;font-weight:700;color:var(--ok-tx)}
.cov-quiet-toggle{
  margin-top:var(--sp-3);background:none;border:1px dashed var(--line);border-radius:999px;
  padding:4px var(--sp-3);font:inherit;font-size:var(--fs-xs);font-weight:600;color:var(--ink2);cursor:pointer;
}
.cov-quiet-toggle:hover{border-color:var(--accent-line);color:var(--accent)}
.cov-quiet-why{margin-left:var(--sp-3);font-size:var(--fs-micro)}
/* A division is a box with a title bar; each marketplace is a LANE inside
 * it — a label column with the platform's colour down its left edge, a
 * divider, then that platform's chips. The chips keep their status colours;
 * the lane, not a chip, carries the platform. */
.cov-div{border:1px solid var(--line);border-radius:var(--radius-sm);overflow:hidden;background:var(--panel)}
.cov-div + .cov-div{margin-top:var(--sp-3)}
.cov-div-head{
  display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--sp-1) var(--sp-3);
  margin:0;padding:8px var(--sp-3);background:var(--line2);border-bottom:1px solid var(--line);
  text-transform:none;letter-spacing:0;font-size:var(--fs-xs);
}
.cov-div-name{font-size:var(--fs-sm);font-weight:750;color:var(--ink)}
.cov-div-sum{display:inline-flex;align-items:center;gap:var(--sp-2)}
.cov-plat-row{display:grid;grid-template-columns:150px minmax(0,1fr);margin:0}
.cov-plat-row + .cov-plat-row{border-top:1px solid var(--line2)}
.cov-plat-label{
  display:flex;flex-direction:column;justify-content:center;gap:1px;
  padding:8px var(--sp-3);background:var(--inset);border-right:1px solid var(--line2);
  box-shadow:inset 4px 0 0 var(--plat-ols);
}
.cov-plat-row.is-olt .cov-plat-label{box-shadow:inset 4px 0 0 var(--plat-olt)}
.cov-plat-name{font-size:var(--fs-xs);font-weight:800;letter-spacing:.04em;color:var(--ink)}
.cov-plat-count{font-size:var(--fs-micro);color:var(--ink3)}
.cov-plat-row .cov-grid{padding:8px var(--sp-3);align-content:center}
@media(max-width:600px){ .cov-plat-row{grid-template-columns:1fr} .cov-plat-label{border-right:none;border-bottom:1px solid var(--line2)} }

/* The store picker row wraps on a phone: the division pills, the picker and
 * the platform badge no longer fit one line of 390px, and the picker's
 * options now carry each store's percentage. */
.store-pick-wrap{flex-wrap:wrap}
.store-pick{max-width:100%}

@media(max-width:600px){ .hero-ringed{flex-wrap:wrap;gap:var(--sp-4)} }

/* ---- print: the page is a record, not a performance ------------------------ */
@media print{
  .busy-bar,.confetti,.cov-quiet-toggle{display:none !important}
  .content > *,.ring-arc,.pilebar .seg,.catbar .fill,.podium-bar span{animation:none !important}
  .rail::before,.topbar::after{display:none}
}

/* ---- the store charts: area, draw-in, the latest point ---------------------
 * (component: components/store/completion-trend.blade.php) */
.trend-area{stroke:none;animation:area-in 1s ease-out .6s both}
@keyframes area-in{from{opacity:0}}
.trend-draw{stroke-dasharray:1;stroke-dashoffset:0;animation:line-draw 1.2s cubic-bezier(.4,0,.2,1) both}
@keyframes line-draw{from{stroke-dashoffset:1}}
.trend-halo{fill:var(--ok-dot);opacity:.2;animation:halo-pop .6s ease-out 1.1s both}
@keyframes halo-pop{from{opacity:0;transform-box:fill-box;transform-origin:center;transform:scale(.3)}}
.trend-dot.is-last{fill:var(--ok-tx);stroke-width:3}
.trend-dot-sub.is-last{stroke-width:2.5}
.trend-endlabel.is-last{
  /* The OK pair inverted — --ok-bg ink on an --ok-tx ground — which every
   * theme already measures in both directions; --on-accent belongs on the
   * accent only (PaletteContrastTest). */
  color:var(--ok-bg);background:var(--ok-tx);padding:2px 8px;border-radius:999px;
  font-size:var(--fs-xs);box-shadow:0 3px 8px color-mix(in srgb,var(--ok-tx) 30%,transparent);
}
.trend-sublabel.is-last{color:var(--input-tx);font-weight:800;font-size:var(--fs-xs)}
.trend-sublabel{color:var(--input-tx)}
@media (prefers-reduced-motion:reduce){ .trend-area,.trend-draw,.trend-halo{animation:none} }
@media print{ .trend-area,.trend-draw,.trend-halo{animation:none !important} }

/* ---- toned summary cards (components/stat.blade.php) ------------------------
 * The action palette's own pairs, so a tone means what that colour already
 * means: red losing (UPDATE STOCK), amber margin (UPDATE PRICE), blue
 * attention (INPUT PRODUCT), green done (OK). */
.card.stat{position:relative;overflow:hidden;padding-left:72px;border-top:3px solid var(--st-dot);background:linear-gradient(135deg,var(--st-bg) 0%,var(--panel) 58%)}
.card.stat .v{color:var(--st-tx)}
.stat-ico{position:absolute;left:20px;top:50%;transform:translateY(-50%);width:38px;height:38px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:var(--panel);color:var(--st-tx);box-shadow:0 0 0 4px color-mix(in srgb,var(--st-dot) 22%,transparent)}
.stat-ico svg{width:19px;height:19px}
.tone-neutral{--st-bg:var(--line2);--st-tx:var(--ink);--st-dot:var(--nr-dot)}
.tone-ok{--st-bg:var(--ok-bg);--st-tx:var(--ok-tx);--st-dot:var(--ok-dot)}
.tone-bad{--st-bg:var(--stock-bg);--st-tx:var(--stock-tx);--st-dot:var(--stock-dot)}
.tone-warn{--st-bg:var(--price-bg);--st-tx:var(--price-tx);--st-dot:var(--price-dot)}
.tone-info{--st-bg:var(--input-bg);--st-tx:var(--input-tx);--st-dot:var(--input-dot)}

/* ---- Price gaps: the % as a two-sided bar -------------------------------- */
a.pill.pill-link{text-decoration:none;white-space:nowrap}
a.pill.pill-link:hover{filter:brightness(.96);box-shadow:0 2px 6px rgba(var(--shadow-rgb),.12)}
.gap-pct-cell{white-space:nowrap}
.gapbar{display:inline-block;position:relative;width:64px;height:6px;border-radius:4px;background:var(--line2);vertical-align:middle;margin-right:8px}
.gapbar::before{content:"";position:absolute;left:50%;top:-2px;bottom:-2px;width:1px;background:var(--line)}
.gapbar > span{position:absolute;top:0;bottom:0;border-radius:3px}
.gapbar.is-over > span{left:50%;background:var(--stock-dot)}
.gapbar.is-under > span{right:50%;background:var(--price-dot)}

/* ---- list entries: two-line names, row actions as small buttons ---------- */
.clamp-2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.row-actions{display:inline-flex;gap:6px;white-space:nowrap}
.row-actions .rowact{border:1px solid var(--line);border-radius:999px;padding:3px 10px;background:var(--control-bg)}
.row-actions .rowact:hover{border-color:var(--accent-line);background:var(--accent-soft)}
.row-actions .rowact.del{color:var(--urgent-tx)}
.row-actions .rowact.del:hover{border-color:var(--urgent-line);background:var(--urgent-bg)}

/* ---- Force list: add (redesigned 2 Oct 2026) -------------------------------
 * One full-width panel (a 960px column left half the screen empty); the
 * paste's columns in a strip right above the box; the preview's counts on
 * one line, its piles as tabs, and Apply at the bottom right. Tokens only. */
.fl-scope{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-3)}
.fl-cols{display:grid;grid-template-columns:1.4fr 1.6fr 3fr repeat(3,.6fr);border:1px solid var(--line);border-bottom:0;
  border-radius:var(--radius-sm) var(--radius-sm) 0 0;background:var(--depth-2);overflow:hidden}
.fl-col{padding:6px var(--sp-2);font-size:var(--fs-micro);font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--ink2);
  border-left:1px solid var(--line2);white-space:nowrap}
.fl-col:first-of-type{border-left:0}
.fl-col.req{color:var(--ink)}
.fl-col em{font-style:normal;font-weight:600;text-transform:none;letter-spacing:0;color:var(--accent);margin-left:4px}
.fl-cols + .fl-paste{border-top-left-radius:0;border-top-right-radius:0}
.fl-hint{font-size:var(--fs-xs);color:var(--ink3);margin:var(--sp-1) 0 var(--sp-2)}
.fl-hint b{color:var(--ink)}
.fl-inline{margin-left:var(--sp-3)}
.fl-under{display:flex;flex-wrap:wrap;align-items:flex-start;gap:var(--sp-3);margin-top:var(--sp-2)}
.fl-under .ex-toggle{margin:0;flex:1 1 300px}
.fl-upload{cursor:pointer}
.fl-upload:focus-within{box-shadow:var(--focus-ring)}
.fl-result{margin-top:var(--sp-4)}
.fl-counts{font-size:var(--fs-sm);color:var(--ink2);margin:0 0 var(--sp-3)}
.fl-counts b{color:var(--ink);font-variant-numeric:tabular-nums}
.fl-bad{color:var(--urgent-tx)}
.fl-counts b.fl-bad{color:var(--urgent-tx)}
.fl-tabs{margin-bottom:var(--sp-3)}
.fl-codes{font-size:var(--fs-xs);line-height:1.7;word-break:break-word}
.fl-apply{display:flex;justify-content:flex-end;align-items:center;gap:var(--sp-3);margin-top:var(--sp-4);
  padding-top:var(--sp-3);border-top:1px solid var(--line2)}
@media(max-width:700px){ .fl-cols{grid-template-columns:repeat(4,1fr)} .fl-col:nth-of-type(5){border-left:0} }

/* ---- the bulk bar and the table search ------------------------------------ */
.bulkbar.standalone{border-top:0;box-shadow:0 -6px 16px rgba(var(--shadow-rgb),.10)}
.bulkbar.standalone::before{content:"";position:absolute;left:0;right:0;top:0;height:3px;background:var(--brand-grad)}
.bulkbar-count{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:22px;padding:0 7px;border-radius:999px;background:var(--brand-fill);color:var(--on-accent);font-weight:800;margin-right:4px}
.panel-head .search{width:320px;min-width:min(240px,100%);max-width:100%;flex:0 1 320px}

/* ---- Approvals: listing-rule requests ------------------------------------ */
.rr-slice{margin-left:6px;font-weight:650}
.rr-old{color:var(--price-tx);font-weight:700}
.rr-old::before{content:"\25CF" / "";margin-right:5px;color:var(--price-dot)}
.rr-decided{list-style:none;display:flex;flex-direction:column;gap:6px}
.rr-decided li{display:grid;grid-template-columns:100px minmax(0,1fr);align-items:center;column-gap:var(--sp-3);padding:8px var(--sp-3);border:1px solid var(--line2);border-radius:var(--radius-sm);background:var(--inset);font-size:var(--fs-xs)}
.rr-outcome{display:flex}
.rr-dec-text{display:flex;align-items:center;flex-wrap:wrap;gap:4px 6px;min-width:0}

/* ---- Change log: a table, grouped under the sorted column ---------------
 * (owner, 6 Oct 2026; Mobbin: Google Ads change history, Klaviyo activity
 * log). Division chips above it narrow to one or several divisions; a
 * heading row starts each day, category, scope or person. */
.cl-chips{display:flex;align-items:center;flex-wrap:wrap;gap:var(--sp-2);padding:var(--sp-2) var(--sp-5);border-bottom:1px solid var(--line2);font-size:var(--fs-xs)}
.cl-chip{border:1px solid var(--line);background:var(--panel);color:var(--ink2);font:inherit;font-weight:600;border-radius:999px;padding:3px 12px;cursor:pointer}
.cl-chip:hover{color:var(--ink)}
.cl-chip.on{background:var(--accent-soft);color:var(--accent);border-color:var(--accent)}
.cl-table td{vertical-align:top}
.cl-table tr.cl-group > th{
  font-size:var(--fs-xs);font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--ink2);
  text-align:left;padding:var(--sp-2) var(--sp-3);background:var(--inset);border-bottom:1px solid var(--line2);
}
.cl-when{white-space:nowrap;font-size:var(--fs-xs);color:var(--ink3);font-variant-numeric:tabular-nums}
.cl-target{font-weight:650;min-width:160px}
.cl-pname{display:block;max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:400;font-size:var(--fs-xs);color:var(--ink2)}
.cl-change{min-width:260px;max-width:560px}
/* A warehouse mapping's summary names two hundred warehouses: two lines,
 * the whole text in the tooltip. */
.cl-summary{color:var(--ink2);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.cl-where,.cl-by{white-space:nowrap}
.cl-verb{display:inline-block;font-size:var(--fs-micro);font-weight:700;text-transform:uppercase;letter-spacing:.04em;margin-bottom:2px}
.cl-verb.is-created{color:var(--ok-tx)}
.cl-verb.is-updated{color:var(--input-tx)}
.cl-verb.is-removed{color:var(--stock-tx)}
.cl-verb.is-restored{color:var(--hold-tx)}
.cl-acts{display:flex;gap:var(--sp-1);flex-wrap:wrap;margin-top:var(--sp-1)}

/* ---- Product drawer: how wide "not ours" goes (owner, 6 Oct 2026) ------- */
.pe-width{border:0;padding:0;margin:0 0 var(--sp-4);display:grid;gap:var(--sp-1)}
.pe-width legend{font-weight:700;margin-bottom:var(--sp-2)}
.pe-width-opt{display:flex;align-items:center;gap:var(--sp-2);padding:var(--sp-2) var(--sp-3);border:1px solid var(--line);border-radius:8px;cursor:pointer;background:var(--panel)}
.pe-width-opt:hover{background:var(--row-hover)}
.pe-width-opt.on{border-color:var(--accent);background:var(--accent-soft)}
.pe-width-text{flex:1;min-width:0;display:flex;flex-direction:column}
.pe-width-slice{font-size:var(--fs-xs);color:var(--ink2)}
.pe-width-n{font-size:var(--fs-xs);color:var(--ink3);white-space:nowrap;font-variant-numeric:tabular-nums}
/* Approvals: "Skip the brand here" under Approve, with how many asks it answers. */
.aq-brand{display:block;margin-top:var(--sp-1);white-space:nowrap}
.aq-brand-n{color:var(--ink3);font-weight:400}

/* ===== RULES TREE (Listing rules, owner 6 Oct 2026) ======================
 * Category → sub-category → brand, a count of the Skips under each line and
 * how many a wider Skip already covers. Mobbin: Square nested rows, Canny
 * grouped counts, AirOps floating selection bar. */
.rt{padding:var(--sp-3) var(--sp-5) var(--sp-5)}
.rt-intro{display:flex;align-items:flex-start;gap:var(--sp-3);justify-content:space-between;flex-wrap:wrap;margin-bottom:var(--sp-3)}
.rt-intro .hint{max-width:72ch;margin:0}
.rt-list,.rt-list ul{list-style:none;margin:0;padding:0}
.rt-line,.rt-item{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap;padding:6px var(--sp-2);border-bottom:1px solid var(--line2)}
.rt-line:hover,.rt-item:hover{background:var(--row-hover)}
.rt-d2{padding-left:28px}.rt-d3{padding-left:52px}.rt-d4{padding-left:76px}
.rt-tog{flex:none;width:22px;height:22px;border:0;background:none;color:var(--ink2);cursor:pointer;border-radius:4px;padding:0}
.rt-tog:hover{background:var(--hover-strong)}
.rt-name{font-weight:650}
.rt-d1 > .rt-name{font-weight:800}
.rt-level{font-size:var(--fs-micro);font-weight:500;color:var(--ink3);text-transform:uppercase;letter-spacing:.04em;margin-left:4px}
.rt-here{font-size:var(--fs-micro);font-weight:700;padding:2px var(--sp-2);border-radius:var(--radius-xs);background:var(--accent-soft);color:var(--accent)}
.rt-here.is-covered{background:var(--line2);color:var(--ink2)}
.rt-n{font-size:var(--fs-xs);color:var(--ink3);margin-left:auto;font-variant-numeric:tabular-nums}
.rt-cov{font-size:var(--fs-micro);font-weight:700;padding:2px var(--sp-2);border-radius:var(--radius-xs);border:1px solid var(--line);color:var(--ink2)}
.rt-item{font-size:var(--fs-sm)}
.rt-nobox{width:13px;flex:none}
.rt-what{min-width:0;display:flex;gap:var(--sp-2);align-items:baseline;cursor:pointer}
.rt-pname{color:var(--ink2);font-size:var(--fs-xs);max-width:360px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rt-by{font-size:var(--fs-xs);color:var(--ink3);margin-left:auto}
/* Primary / Secondary / Skip on a line: the pd-seg look, smaller. */
.rt-set{display:inline-flex;border:1px solid var(--line);border-radius:6px;overflow:hidden}
.rt-set button{border:0;background:var(--panel);color:var(--ink2);font:inherit;font-size:var(--fs-micro);font-weight:600;padding:2px 8px;cursor:pointer}
.rt-set button + button{border-left:1px solid var(--line)}
.rt-set button:hover{color:var(--ink);background:var(--row-hover)}
.rt-list.is-busy{opacity:.6;transition:opacity .15s}
/* Scoped: this codebase has no global [x-cloak] rule (see .stale-item-list). */
.rt-selbar[x-cloak]{display:none}
.rt-selbar{position:sticky;bottom:var(--sp-3);margin:var(--sp-3) auto 0;width:fit-content;display:flex;align-items:center;gap:var(--sp-3);
  padding:var(--sp-2) var(--sp-4);background:var(--panel);border:1px solid var(--line);border-radius:999px;box-shadow:0 6px 20px rgba(var(--shadow-rgb),.18)}
/* The prompt after a Skip: a panel in the corner, not a modal: it asks, and the page stays usable. */
/* Top right, clear of the topbar: the bottom is where page toasts land, and one covered this text (owner, 7 Oct 2026). */
.cp-panel{position:fixed;right:var(--sp-5);top:calc(var(--sp-5) + 64px);z-index:60;width:min(420px,calc(100vw - 32px));display:grid;gap:var(--sp-2);
  padding:var(--sp-4);background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:0 10px 30px rgba(var(--shadow-rgb),.22)}
.cp-head{display:flex;align-items:center;justify-content:space-between}
.cp-panel p{margin:0}
.cp-acts{display:flex;gap:var(--sp-2);justify-content:flex-end}

/* ---- Data management: today's schedule as a 24-hour strip -------------- */
.sched{margin-bottom:var(--sp-4)}
.sched-body{padding:var(--sp-8) var(--sp-8) var(--sp-5)}
.sched-track{position:relative;height:164px;border-top:2px solid var(--line);margin-top:6px;
  background:linear-gradient(90deg,var(--inset),var(--panel) 25%,var(--panel) 75%,var(--inset))}
.sched-hour{position:absolute;top:-24px;transform:translateX(-50%);font-size:var(--fs-micro);color:var(--ink3);font-variant-numeric:tabular-nums}
.sched-hour::after{content:"";position:absolute;left:50%;top:22px;width:1px;height:8px;background:var(--line)}
.sched-now{position:absolute;top:-6px;bottom:0;width:2px;background:var(--brand-grad);transform:translateX(-1px);border-radius:2px}
.sched-now > span{position:absolute;bottom:-18px;left:50%;transform:translateX(-50%);font-size:var(--fs-micro);font-weight:800;color:var(--accent);text-transform:uppercase;letter-spacing:.05em}
.sched-job{position:absolute;top:0;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;gap:2px;text-align:center;width:130px}
.sched-job.lane-1{top:52px}
.sched-job.lane-2{top:104px}
.sched-job.at-start{transform:none;align-items:flex-start;text-align:left}
.sched-job.at-end{transform:translateX(-100%);align-items:flex-end;text-align:right}
.sched-pin{width:12px;height:12px;border-radius:50%;background:var(--panel);border:3px solid var(--accent);margin-top:-7px;box-shadow:0 0 0 3px var(--panel)}
.sched-job.lane-1 .sched-pin,.sched-job.lane-2 .sched-pin{margin-top:0}
.sched-time{font-size:var(--fs-xs);font-weight:800;color:var(--ink);font-variant-numeric:tabular-nums}
.sched-name{font-size:var(--fs-micro);color:var(--ink2);line-height:1.3;display:inline-flex;align-items:center;gap:5px}
.sched-job.is-past .sched-pin{border-color:var(--line)}
.sched-job.is-past .sched-time,.sched-job.is-past .sched-name{color:var(--ink3)}
.sched-job.is-running .sched-pin{border-color:var(--ok-dot);background:var(--ok-bg)}
.sched-job.is-running .sched-time{color:var(--ok-tx)}
.sched-job.is-running .sched-name{color:var(--ok-tx);font-weight:700}
@media(max-width:900px){ .sched-body{padding:var(--sp-8) var(--sp-4) var(--sp-5)} .sched-job{width:90px} }

/* ---- Warehouse mapping → Marketplace: the chosen platform in its own colour
 * (page audit, 26 Sep 2026). The generic .div-btn.on brand fill made Shopee
 * and Tokopedia look the same once chosen; a choice OF a platform wears that
 * platform's badge pair. */
.div-btn.on.mk-shopee{background:var(--plat-ols-bg);color:var(--plat-ols-tx);border-color:var(--plat-ols-tx);box-shadow:none}
.div-btn.on.mk-tokopedia{background:var(--plat-olt-bg);color:var(--plat-olt-tx);border-color:var(--plat-olt-tx);box-shadow:none}

/* ---- a link with no class of its own takes the accent (page audit,
 * 26 Sep 2026). There was no base link colour at all, so every unclassed
 * link — Price gaps' store codes among them — fell back to the browser's
 * default blue: off-palette in the light themes and near-invisible (dark
 * blue on near-black) in Dark. --accent is measured against --panel in every
 * theme by PaletteContrastTest. */
.content a:not([class]){color:var(--accent);text-underline-offset:2px}
.content a:not([class]):hover{color:var(--accent-hover)}

/* ---- the page arrives rather than appears -------------------------------- */
@keyframes rise-in{from{opacity:0}}
/* OPACITY ONLY, never a transform: a transformed wrapper becomes the
 * containing block for every position:fixed descendant, so while it was
 * moving (and for as long as a background tab holds the animation paused)
 * the rule drawer and quick-rule panel scrolled with the page and pushed a
 * horizontal scrollbar. A fade cannot do that. */
.content > *{animation:rise-in .35s cubic-bezier(.2,.8,.2,1) backwards}

@media (prefers-reduced-motion:reduce){
  .content > *{animation:none}
  .busy-bar.is-on::before{animation:none;left:0;width:100%;opacity:.6}
  .tier3-btn,.subtab{transition:none}
}

/* ===== LISTING POLICY — the category tick boxes
 *
 * A GRID, not a list: a brand can be in 36 categories and a single column of
 * 36 checkboxes is a scroll rather than a choice. auto-fill with a 200px
 * floor means the count decides the columns, so one category and thirty-six
 * both read sensibly.
 *
 * No new hue — it reuses --line, --line2 and --ink3, which already carry
 * their own colourblind counterparts. */

/* The Sell tick boxes sit INSIDE the add bar, beside the other fields, so
 * they need a field's shape rather than the grid .lp-cats gets.
 *
 * A WRAPPING ROW, NOT A COLUMN, and the difference is the four families.
 * It was a column when the question was NR-or-PR and two boxes stacked to
 * about a field's height. Four stack to three times that, which drags the
 * add bar's baseline down and leaves the Why box floating against the middle
 * of the list. The legend takes the whole first line so the boxes still read
 * as one labelled group.
 *
 * SELECTED AS `.addbar .lp-sell`, NOT `.lp-sell`, and the extra class is
 * load-bearing: the fieldset also carries .fld, and `.addbar .fld` sets
 * flex-direction:column at specificity (0,2,0). A bare `.lp-sell` loses to
 * it, so the row silently stayed a column and the rule looked like a
 * caching problem rather than a losing one. */

/* The count beside a brand in the picker: "this one already has rules".
 * A number rather than a dot, because "ASUS 17" says a decision was made
 * about it in DETAIL, where a dot says only that one was made at all.
 * It sits against the NAME, not the edge of its 200px grid cell — floated
 * right it read as belonging to the brand in the next column along. */

/* Rules bundled by brand, replacing a flat table that showed 51 rows for
 * three decisions (owner, 12 Sep 2026). Each block is one sentence about
 * one supplier; the categories under it are that sentence's SCOPE, not
 * seventeen more rules. */


.sr-filters{padding:var(--sp-3) var(--sp-5) 0;flex-wrap:wrap}
.sr-group{font-size:var(--fs-micro);font-weight:700;color:var(--ink3);letter-spacing:.04em;text-transform:uppercase;padding:var(--sp-4) var(--sp-5) var(--sp-1);margin:0}
/* The Rules table (owner, 2 Oct 2026): one row per category · brand, each
 * setting changed in its own cell; the shop's own exact setting carries a
 * small withdraw (×). */
.sr-table td{vertical-align:middle}
.sr-cat{font-weight:600;color:var(--ink)}
.sr-cell{min-width:0}
.sr-cell .tier3,.sr-cell .ar-src,.sr-val{vertical-align:middle}
.sr-val{font-weight:600;color:var(--ink);margin-right:var(--sp-1)}
.sr-x{margin-left:2px;font-size:var(--fs-md);line-height:1;padding:0 4px}
.sr-floor{display:inline-flex;align-items:center;gap:var(--sp-1);margin-left:var(--sp-2);vertical-align:middle}
.sr-codes{display:inline-flex;margin:0;vertical-align:middle}
.sr-other{padding:var(--sp-2) var(--sp-5) var(--sp-4)}

/* ===== SHARED LAYOUT HELPERS (UI review, 27 Sep 2026)
 *
 * Classes for spacing that views used to write inline, one style="" at a
 * time, each a little different. One class per repeated shape, so a new
 * screen copies a class name rather than the last screen's inline style.
 */
/* A panel's content area when it is not a table: the same inset as .panel-head. */
.panel-body{padding:var(--sp-5)}
/* A section heading OUTSIDE a panel (the dashboards' "What needs doing"):
 * a .panel-head without the rule and the side inset. */
.section-head{padding:var(--sp-5) 0 0;border:none}
.section-head .tools{font-size:var(--fs-xs)}
/* A small second line under a cell's value: "not set — defaulting to Shopee". */
.meta-line{display:block;font-size:var(--fs-micro)}
/* The one-line explanation under a panel heading ("Units sold this week
 * against an ordinary week..."): smaller than the heading, muted beside it. */
.panel-sub{font-size:var(--fs-xs)}
/* A form field that takes a whole row of an .rf-row. */
.fld-full{flex-basis:100%}
/* A .foot-note holding a block of lines rather than one row of them. */
.foot-note.stacked{flex-direction:column;align-items:flex-start;gap:var(--sp-1)}
/* Helper text inside a body, a form or a table cell. .foot-note is a panel
 * FOOTER (rule, inset, flex row), and used as a hint it drew a stray line
 * and split "the <b>Default</b> theme" into gapped flex items. */
.hint{display:block;font-size:var(--fs-xs);color:var(--ink3);margin:var(--sp-1) 0 var(--sp-3)}
td .hint{margin:var(--sp-1) 0 0}
/* A bullet list inside a note or an alert. */
.note-list{margin:var(--sp-2) 0 0 var(--sp-5)}
/* The sign-in page's paragraphs, and its one checkbox row. */
.login-card .login-note + .login-note,.login-card form + .login-note{margin-top:var(--sp-4)}
.login-card .fld-check{display:flex;align-items:center;gap:var(--sp-2)}
.login-card .fld-check > input{width:auto}
.login-card .fld-check > label{margin:0}
/* The register page's explanation, above its form. */
.login-card p.login-note{margin-bottom:var(--sp-4)}
/* A ghost button that is a TOGGLE ("Hide finished"): pressed reads as
 * pressed by fill and ring as well as by the aria state, never by colour
 * alone. */
.btn-ghost[aria-pressed="true"]{background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--accent)}
/* The pager sits at the end of its foot-note row. */
.pager{float:right}


/* ===================================================== INSTANT FEEDBACK
 * The first 100ms after a click (owner, 1 Oct 2026: "give the illusion of
 * speeding up"). Livewire marks the element a request came from with
 * `data-loading` the moment the request leaves and removes it when the
 * answer lands, so these rules cannot drift from what the server did:
 *   - a warehouse heading's arrow turns at once;
 *   - a pressed Primary / Secondary / Skip segment lights at once, and its
 *     two neighbours dim (the whole light used to fade, pressed one too);
 *   - anything waiting on the server shows a progress cursor.
 * The server's answer still decides what the row finally says. */
[data-loading]{cursor:progress}
.wh-disc-mark{display:inline-block;transition:transform .12s ease}
.wh-disc[aria-expanded="false"][data-loading] .wh-disc-mark{transform:rotate(90deg)}
.wh-disc[aria-expanded="true"][data-loading] .wh-disc-mark{transform:rotate(-90deg)}
.tier3.is-busy{opacity:1}
.tier3.is-busy .tier3-btn:not([data-loading]){opacity:.45}
.tier3-btn[data-loading]{box-shadow:inset 0 0 0 2px currentColor}
.tier3-btn.tier3-primary[data-loading]{background:var(--ok-bg)}
.tier3-btn.tier3-secondary[data-loading]{background:var(--price-bg)}
.tier3-btn.tier3-skip[data-loading]{background:var(--stock-bg)}
/* Livewire's own bar while the rail switches a page in place: the accent,
 * not its built-in blue. `html` outranks the style tag it injects later. */
html #nprogress .bar{background:var(--accent)}
html #nprogress .peg{box-shadow:0 0 10px var(--accent),0 0 5px var(--accent)}
@media (prefers-reduced-motion:reduce){
  .wh-disc-mark{transition:none}
}

/* ===== DOWNLOAD MENU — the worklist's Download Excel ▾ (2 Oct 2026):
 * this tab, or every task for the store in one sheet. Absolute inside
 * .dl-menu, which carries no transform. Tokens only. */
.dl-menu{position:relative;display:inline-block}
.dl-pop{position:absolute;z-index:40;top:calc(100% + 6px);right:0;width:300px;
  background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:var(--sp-1);
  box-shadow:0 12px 32px rgba(var(--shadow-rgb),.18)}
.dl-item{display:flex;flex-direction:column;gap:2px;width:100%;text-align:left;background:none;border:0;
  border-radius:8px;padding:var(--sp-2) var(--sp-3);font:inherit;cursor:pointer;color:var(--ink)}
.dl-item:hover,.dl-item:focus-visible{background:var(--row-hover)}
.dl-title{font-weight:650;font-size:var(--fs-sm)}
.dl-sub{font-size:var(--fs-xs);color:var(--ink3)}

/* ===== RESTOCK PLANNER — a product MPL no longer carries (3 Oct 2026)
 *
 * Owner: "for those that are not in the MPL, it should have a special
 * effect", after the words under the code were removed. An edge stripe and
 * a tinted row (Mobbin: Airtable's row colour bar), NOT a dimmed row like
 * Rox's "Unavailable": these are often the products most worth chasing. The
 * amber pair is the existing --price-* tokens, so every theme has it. The
 * row also carries a title and an .sr-only phrase: colour alone is not a
 * fact a screen reader can hear. */
/* Order Forecast's table, made easier to read (owner, 3 Oct 2026: "a bit
 * hard to see the table"; Mobbin: Shopify Inventory, Xero Products): solid
 * ink instead of greys for every value, the product name a step bolder,
 * the code small and mono, zeros quiet so real figures stand out, a faint
 * stripe to follow a row across fourteen columns, compact rows. */
/* Order Forecast: the tabs with the PR / NR switch at their end. */
.rs-tabbar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--sp-3);margin-bottom:var(--sp-4)}
.rs-tabbar .subtabs{margin-bottom:0}
.rs-table td{color:var(--ink);font-size:var(--fs-sm);padding-top:7px;padding-bottom:7px;vertical-align:middle}
.rs-table tbody tr:nth-child(even) > td{background:var(--row-hover)}
.rs-table tbody tr:hover > td{background:var(--hover-strong)}
.rs-table .rs-cat{color:var(--ink2);font-size:var(--fs-xs);font-weight:500;white-space:nowrap}
.rs-table .rs-brand{font-weight:600;white-space:nowrap}
.rs-table .rs-prod{min-width:260px;max-width:420px}
.rs-name{display:block;font-weight:600;line-height:1.3}
/* The name and its copy button side by side: wrapped, the name stays one block
 * and the button sits at its first line, never alone between name and code. */
.rs-name:has(> .pd-copy),.rs-part-name:has(> .pd-copy){display:flex;align-items:flex-start;gap:2px}
.rs-name > .pd-copy,.rs-part-name > .pd-copy{margin-top:-3px}
/* Order Forecast names stay on ONE line (owner, 6 Oct 2026), cut with an
 * ellipsis; the full name is the hover title and what Copy copies. */
.rs-one{min-width:0;max-width:520px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Column widths (owner, 6 Oct 2026): a grip on each heading's right edge.
 * Sized, the table is fixed-layout at the saved widths, and the name gives
 * way to its column rather than to the 520px cap. */
.rs-table th{position:relative}
.col-grip{position:absolute;top:0;right:-3px;bottom:0;width:7px;cursor:col-resize;z-index:3;touch-action:none}
.col-grip::after{content:"";position:absolute;top:25%;bottom:25%;left:3px;width:1px;background:var(--line)}
.col-grip:hover::after,.is-col-resizing .col-grip::after{background:var(--accent);width:2px}
.is-col-resizing{cursor:col-resize;user-select:none}
.rs-table.rs-sized{table-layout:fixed}
.rs-table.rs-sized th,.rs-table.rs-sized td{overflow:hidden;text-overflow:ellipsis}
.rs-table.rs-sized .rs-prod{min-width:0;max-width:none}
.rs-sized .rs-one{max-width:none}
.rs-name:has(> .rs-one),.rs-part-name:has(> .rs-one){align-items:center}
.rs-name > .rs-one ~ .pd-copy,.rs-part-name > .rs-one ~ .pd-copy{margin-top:0}
.rs-code{display:block;margin-top:1px;font-family:ui-monospace,Consolas,monospace;font-size:var(--fs-micro);color:var(--ink3)}
.rs-table .num{font-variant-numeric:tabular-nums;font-weight:500}
.rs-table .rs-soh{font-weight:700}
.rs-table .rs-price{color:var(--ink2);font-weight:400}
/* PR + NR of one model (owner, 6 Oct 2026): the summed row, its codes under it. */
.rs-table .rs-pair .rs-prod{position:relative;padding-left:28px;cursor:pointer;user-select:none}
.rs-twin-tog{flex:none;margin-left:-24px;margin-right:4px;width:20px;height:20px;border:0;border-radius:var(--radius-xs);background:none;color:var(--ink2);cursor:pointer;padding:0}
.rs-twin-tog:hover{background:var(--hover-strong)}
.rs-twin-tog:focus-visible{box-shadow:var(--focus-ring)}
.rs-twin-ind{display:inline-block;transition:transform .15s}
.rs-twin-tog[aria-expanded="false"] .rs-twin-ind{transform:rotate(-90deg)}
/* Open, the product row is the parent: a tinted band, an accent edge and
 * bold figures; its codes sit under it plain, indented and quieter, the
 * last one closing the group with a rule (owner, 6 Oct 2026: "easier to
 * visually identify which is the main one"; Mobbin: Midday, Quicken). */
.rs-table tbody tr.rs-pair.is-open > td{background:var(--accent-soft);border-top:1px solid var(--accent-line)}
.rs-table tbody tr.rs-pair.is-open > td:first-child{box-shadow:inset 3px 0 0 var(--accent)}
.rs-table tr.rs-pair.is-open .num,.rs-table tr.rs-pair.is-open .rs-name{font-weight:700}
.rs-table tbody tr.rs-part > td{background:var(--panel);padding-top:4px;padding-bottom:4px;font-size:var(--fs-xs);color:var(--ink2)}
.rs-table tr.rs-part .num{font-weight:400}
.rs-table tbody tr.rs-part:has(+ tr:not(.rs-part)) > td,.rs-table tbody tr.rs-part:last-child > td{border-bottom:1px solid var(--accent-line)}
.rs-table .rs-part .rs-prod{padding-left:44px;position:relative}
.rs-table .rs-part .rs-prod::before{content:"";position:absolute;left:22px;top:0;bottom:50%;width:12px;border-left:1px solid var(--accent-line);border-bottom:1px solid var(--accent-line)}
.rs-table .rs-part .rs-code{font-size:var(--fs-xs);color:var(--ink2)}
.rs-part-name{display:block;color:var(--ink3);font-size:var(--fs-micro);line-height:1.3}
.rs-zero{color:var(--ink3);font-weight:400;opacity:.6}
.rs-need{display:inline-block;min-width:34px;text-align:center;padding:2px 8px;border-radius:999px;
  background:var(--input-bg);color:var(--input-tx);font-weight:700}
tr.rs-gone > td{background:var(--price-bg)}
tr.rs-gone > td:first-child{box-shadow:inset 4px 0 0 var(--price-tx)}
/* My focus editor: two searchable checkbox lists side by side, stacked on
 * a phone (Mobbin: Shopify's column picker, Supabase's Save/Reset). */
/* Redesigned 3 Oct 2026 (Mobbin: Juicebox chips + live count + Save top
 * right; Dribbble's pill toggles in a grid). */
.rs-focus-cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:var(--sp-4);padding:var(--sp-3) var(--sp-4) var(--sp-4)}
.rs-focus-stack{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0}
.rs-focus-col{border:0;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--sp-2);min-width:0}
.rs-focus-head{display:flex;justify-content:space-between;align-items:baseline;gap:var(--sp-2);flex-wrap:wrap}
.rs-focus-title{font-weight:600}
.rs-focus-acts{display:flex;gap:var(--sp-3)}
/* Folding brand blocks (owner: "it extends super long"; Mobbin: Vimeo's
 * collapsible filter sections). Folded = one line with what is ticked. */
.rs-fold{border:1px solid var(--line);border-radius:8px;padding:var(--sp-2) var(--sp-3)}
.rs-fold.is-closed .rs-focus-head{flex-wrap:nowrap}
.rs-fold-btn{display:flex;align-items:baseline;gap:6px;background:none;border:0;padding:0;cursor:pointer;font:inherit;color:inherit;text-align:left}
.rs-fold-chev{width:1em;color:var(--ink3)}
.rs-fold-sum{font-size:var(--fs-xs);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.rs-focus-count{font-size:var(--fs-sm);font-weight:600;font-variant-numeric:tabular-nums}
.rs-chips{display:flex;flex-wrap:wrap;gap:6px}
.rs-chip{border:1px solid var(--accent);background:var(--panel);color:var(--accent);border-radius:999px;
  padding:2px 10px;font-size:var(--fs-xs);font-weight:600;cursor:pointer;font-family:inherit}
.rs-chip:hover{background:var(--row-hover)}
.rs-focus-list{max-height:280px;overflow:auto;border:1px solid var(--line);border-radius:8px;padding:var(--sp-2);
  display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:6px;align-content:start}
.rs-pill{display:flex;gap:6px;align-items:center;font-size:var(--fs-xs);cursor:pointer;border:1px solid var(--line);
  border-radius:6px;padding:4px 8px;min-width:0}
.rs-pill span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rs-pill:hover{background:var(--row-hover)}
.rs-pill:has(input:checked){border-color:var(--accent);background:var(--row-hover);font-weight:600}
/* Sales trend in words (owner, 3 Oct 2026: the bar chart was "not very
 * intuitive to new user"). The word is the meaning; colour repeats it. */
.rs-trend-cell{white-space:nowrap}
.rs-trend{font-size:var(--fs-xs);font-weight:600}
.rs-trend.is-up{color:var(--ok-tx)}
.rs-trend.is-down{color:var(--urgent-tx)}
.rs-trend.is-flat{color:var(--ink2);font-weight:400}
.rs-trend.is-quiet{color:var(--ink3);font-weight:400}
/* Days cover as a runway (9 Oct 2026, replacing the battery): the days over
 * a thin bar whose middle tick is the cover target. Red under a week,
 * plain short of the target, green enough, amber over 90 days (the
 * "sitting still" amber). */
.rs-cover{display:inline-flex;flex-direction:column;align-items:flex-end;gap:3px;white-space:nowrap;font-variant-numeric:tabular-nums}
.rs-runway{position:relative;display:block;width:64px;height:4px;border-radius:2px;background:var(--line2)}
.rs-runway span{position:absolute;left:0;top:0;bottom:0;border-radius:2px;background:currentColor}
.rs-runway::after{content:"";position:absolute;left:50%;top:-2px;bottom:-2px;width:2px;background:var(--ink2);box-shadow:0 0 0 1px var(--panel)}
.rs-cover.is-urgent{color:var(--urgent-tx);font-weight:600}
.rs-cover.is-short{color:var(--ink2)}
.rs-cover.is-ok{color:var(--ok-tx)}
.rs-cover.is-over{color:var(--price-tx)}
.rs-cover.is-none{color:var(--ink3)}.rs-gone-key{display:inline-block;width:12px;height:12px;vertical-align:-1px;margin-right:4px;
  border-radius:2px;background:var(--price-bg);box-shadow:inset 3px 0 0 var(--price-tx)}

/* ===== PURCHASE DASHBOARD (3 Oct 2026)
 *
 * Redesigned the same day (owner: "the information is good, but it's
 * kinda ugly"). Mobbin: Profound's ranked list with inline bars, AirOps'
 * numbered ranking, Featurebase's tinted value bars, Framer's stat strip,
 * Programa's urgent group with a coloured edge. No underlines: a whole row
 * is the link and lights on hover. Tokens only. */
.pd-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(420px,100%),1fr));gap:var(--sp-4);margin-bottom:var(--sp-4);align-items:start}
.pd-grid > .panel{margin-bottom:0;min-width:0}
/* The right column: movers above stock health, together as tall as Act today. */
.pd-side{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0}
.pd-side > .panel{margin-bottom:0}
/* The 3px state edge DESIGN.md documents (it was 4px, the only one). */
.pd-urgent{box-shadow:inset 3px 0 0 var(--urgent-tx)}

/* "SOH from" picker: a dropdown panel of warehouses grouped by city. */
.soh-pick{position:relative}
.soh-btn b{font-weight:600}
.soh-panel{position:absolute;z-index:30;top:calc(100% + 6px);right:0;width:min(640px,calc(100vw - 32px));max-height:min(560px,70vh);
  overflow:auto;background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:0 12px 32px rgba(var(--shadow-rgb),.18);padding:var(--sp-3)}
.soh-top{display:flex;justify-content:space-between;align-items:flex-start;gap:var(--sp-3);margin-bottom:var(--sp-3)}
.soh-acts{display:flex;gap:var(--sp-2);align-items:center}
.soh-presets{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-bottom:var(--sp-3)}
.soh-presets .filter{flex:1;min-width:160px}
.soh-cities{display:flex;flex-direction:column;gap:var(--sp-3)}
.soh-city-head{border:0;background:none;padding:0 0 4px;font:inherit;font-size:var(--fs-xs);font-weight:700;letter-spacing:.04em;cursor:pointer;color:var(--ink2)}
.soh-city-head:hover{color:var(--accent)}
.soh-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(96px,1fr));gap:4px}
/* Act today's PR / NR / PR & NR switch: one segmented control. */
.pd-seg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden}
.pd-seg button{border:0;background:var(--panel);color:var(--ink2);font:inherit;font-size:var(--fs-xs);font-weight:600;
  padding:5px 12px;cursor:pointer;white-space:nowrap}
.pd-seg button + button{border-left:1px solid var(--line)}
.pd-seg button:hover{color:var(--ink)}
/* SELECTED IS THE APP'S SELECTED, NOT URGENT RED (critique, 4 Oct 2026): red
 * is for refusals and failures, so a chosen filter read as an error. The
 * same wash and inset ring as a pressed ghost button. Red stays on the
 * panel edge and "Out of stock", where it means what it says. */
.pd-seg button.on{background:var(--accent-soft);color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
/* Act today's category pills: wrap onto a second line rather than scroll. */
.pd-pills{display:flex;flex-wrap:wrap;gap:6px;padding:var(--sp-3) var(--sp-4) var(--sp-2)}
.pd-pill{flex:none;border:1px solid var(--line);background:var(--panel);color:var(--ink2);border-radius:999px;
  padding:4px 10px;font:inherit;font-size:var(--fs-xs);font-weight:600;cursor:pointer;white-space:nowrap}
.pd-pill:hover{border-color:var(--accent-line);color:var(--ink)}
.pd-pill.on{background:var(--accent-soft);border-color:var(--accent);color:var(--accent)}
/* The focus categories with nothing urgent, folded behind one quiet toggle. */
.pd-quiet{display:contents}
.pd-quiet [x-cloak]{display:none}
.pd-pill-more{border-style:dashed;font-weight:400;color:var(--ink3)}
.pd-quiet-list{flex-basis:100%;font-size:var(--fs-xs);color:var(--ink3)}
.pd-pill .ct{margin-left:4px;font-weight:400;opacity:.8;font-variant-numeric:tabular-nums}
/* Act today: one grid template for the header and every row. */
.pd-rows{padding:0 var(--sp-2) var(--sp-2)}
.pd-row{display:grid;grid-template-columns:28px minmax(0,1fr) 104px 112px 60px 104px;gap:var(--sp-3);align-items:center;
  padding:9px var(--sp-2);border-radius:8px;color:inherit;text-decoration:none}
.pd-rows > .pd-row:not(.pd-row-head):hover{background:var(--row-hover)}
.pd-row + .pd-row{border-top:1px solid var(--line)}
/* No Cost column for a reader who may not see cost. */
.pd-rows.no-cost .pd-row{grid-template-columns:28px minmax(0,1fr) 104px 112px 60px}
.pd-rows > .pd-row:hover + .pd-row{border-top-color:transparent}
.pd-row-head{font-size:var(--fs-micro);text-transform:uppercase;letter-spacing:.05em;color:var(--ink3);padding-block:4px}
.pd-rank{width:24px;height:24px;border-radius:50%;display:grid;place-items:center;font-size:var(--fs-xs);font-weight:600;
  background:var(--inset);color:var(--ink2);border:1px solid var(--line);font-variant-numeric:tabular-nums}
.pd-name{min-width:0;display:flex;flex-direction:column;font-size:var(--fs-sm);font-weight:500}
/* Name and code each on a line, the copy button right after the text. */
.pd-line{display:flex;align-items:center;gap:2px;min-width:0}
/* The name may take TWO lines: one-line ellipsis cut "AMD CPU PROCESSOR
 * RYZEN 5 5600-TRAY" and "…5600GT-TRAY" to the same text (critique). */
.pd-line > a{min-width:0;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
  line-clamp:2;color:inherit;text-decoration:none;line-height:1.35}
.pd-line > a:hover{color:var(--accent);text-decoration:underline}
/* A code NEVER wraps (DESIGN.md): it had been breaking at its hyphens. */
.pd-code{font-family:var(--font-mono);font-size:var(--fs-micro);font-weight:400;color:var(--ink3)}
.pd-code-v{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* SOH and 30-day sales under the code: the two figures behind Need. */
.pd-meta{font-size:var(--fs-micro);font-weight:400;color:var(--ink2);font-variant-numeric:tabular-nums}
.pd-copy{flex:none;display:inline-grid;place-items:center;width:24px;height:24px;margin-left:2px;padding:0;border:0;border-radius:6px;
  background:none;color:var(--ink3);cursor:pointer}
.pd-copy svg{width:13px;height:13px}
.pd-copy [x-cloak]{display:none}
.pd-copy:hover{background:var(--line);color:var(--ink)}
.pd-copy:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.pd-copy.is-ok{color:var(--ok-tx)}
.pd-copy.is-err{color:var(--urgent-tx)}
.pd-copy.has-cap{width:auto;gap:3px;padding:0 6px;grid-auto-flow:column}
.pd-copy-cap{font-size:var(--fs-micro);font-weight:600}
/* Act today: each code of a merged product, its family and its own stock. */
.pd-code .lk-fam{margin-right:4px;font-size:var(--fs-micro)}
.pd-code-soh{margin-left:6px;color:var(--ink3);font-size:var(--fs-micro)}
.pd-oos{font-size:var(--fs-xs);font-weight:600;color:var(--urgent-tx);background:var(--urgent-bg);border-radius:999px;padding:2px 9px;white-space:nowrap}
.pd-need{font-weight:700;font-size:var(--fs-base);font-variant-numeric:tabular-nums}
.pd-cost{font-size:var(--fs-sm);color:var(--ink2);font-variant-numeric:tabular-nums;white-space:nowrap}
/* No M0: said, not dashed, with the reason on hover and focus. */
.pd-nom0{font-size:var(--fs-xs);color:var(--ink3);text-decoration:underline dotted;text-underline-offset:3px;cursor:help}
.pd-nom0:focus-visible{outline:none;box-shadow:var(--focus-ring);border-radius:4px}
.pd-spike{margin-left:6px;font-size:var(--fs-micro);font-weight:600;color:var(--price-tx);background:var(--price-bg);border-radius:999px;padding:0 7px}
.rs-trend.is-spike{color:var(--price-tx)}

/* Biggest movers: stacked lists, name | bar to scale | value. */
.pd-movers{padding:0 var(--sp-3) var(--sp-3);display:flex;flex-direction:column;gap:var(--sp-3)}
/* One block per direction: coloured edge, tinted header. */
.pd-mgroup{border:1px solid var(--line);border-left-width:3px;border-radius:8px;overflow:hidden}
.pd-mgroup.is-up{border-left-color:var(--ok-tx)}
.pd-mgroup.is-down{border-left-color:var(--urgent-tx)}
.pd-mhead{margin:0;padding:6px var(--sp-3);font-size:var(--fs-xs);font-weight:700;text-transform:uppercase;letter-spacing:.05em}
.pd-mgroup.is-up .pd-mhead{background:var(--ok-bg);color:var(--ok-tx)}
.pd-mgroup.is-down .pd-mhead{background:var(--urgent-bg);color:var(--urgent-tx)}
.pd-mgroup .pd-mover + .pd-mover{border-top:1px solid var(--line);border-radius:0}
.pd-sub{font-size:var(--fs-micro);text-transform:uppercase;letter-spacing:.05em;color:var(--ink3);margin:var(--sp-3) var(--sp-2) 2px;font-weight:600}
.pd-sub:first-child{margin-top:0}
.pd-mover{display:grid;grid-template-columns:minmax(0,1fr) 30% 76px;gap:var(--sp-3);align-items:center;
  padding:7px var(--sp-2);border-radius:8px;color:inherit;text-decoration:none;font-size:var(--fs-sm)}
a.pd-mover:hover{background:var(--row-hover)}
.pd-mover-name{min-width:0;display:flex;align-items:baseline;gap:6px}
.pd-mover-n{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pd-mover-soh{flex:none;font-size:var(--fs-micro);color:var(--ink3);font-variant-numeric:tabular-nums}
.pd-mover-soh.is-out{color:var(--urgent-tx);font-weight:600}
.pd-mover-bar{height:8px;border-radius:4px;background:var(--line);overflow:hidden}
.pd-mover-bar span{display:block;height:100%;border-radius:4px}
.pd-mover-bar.is-up span{background:var(--ok-tx)}
.pd-mover-bar.is-down span{background:var(--urgent-tx)}
.pd-mover-val{text-align:right;font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap}
.pd-mover-val small{font-size:var(--fs-micro);font-weight:400;color:var(--ink3);margin-left:1px}
.pd-mover-val.is-up{color:var(--ok-tx)}
.pd-mover-val.is-down{color:var(--urgent-tx)}
.pd-empty{padding:6px var(--sp-2);font-size:var(--fs-sm)}

/* Stock health: a bar, and a stat per bucket under it. */
.pd-health{padding:var(--sp-3) var(--sp-4) var(--sp-4)}
.pd-bar{display:flex;height:14px;border-radius:999px;overflow:hidden;gap:2px;background:var(--line)}
.pd-bar span{display:block;min-width:4px}
.pd-legend{list-style:none;margin:var(--sp-4) 0 0;padding:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:var(--sp-3)}
.pd-legend li{display:flex;flex-direction:column;gap:2px;padding-left:var(--sp-3);border-left:1px solid var(--line);font-size:var(--fs-xs)}
.pd-legend-k{display:flex;align-items:center;gap:6px;color:var(--ink2)}
.pd-legend-v{font-size:var(--fs-md);white-space:nowrap;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:-.01em}
.pd-dot{width:9px;height:9px;border-radius:50%;display:inline-block;flex:none}
.h-short{background:var(--urgent-tx)}
.h-ok{background:var(--ok-tx)}
.h-mid{background:var(--ok-tx);opacity:.45}
.h-over{background:var(--price-tx)}
.h-dead{background:var(--ink3)}

/* Category / brand tables: plain names, a tinted bar behind "To buy". */
.pd-link{color:inherit;text-decoration:none;font-weight:600}
.pd-link:hover{color:var(--accent);text-decoration:underline}
.pd-buy{position:relative;min-width:150px}
/* A thin bar UNDER the amount, to scale. It was a 22px tint behind the
 * figure, which peeked out past the text as a stray sliver whenever it was
 * narrower than the number (critique, 4 Oct 2026). A -dot hue is a shape. */
.pd-buy-bar{position:absolute;right:var(--sp-3);bottom:5px;height:4px;border-radius:var(--radius-xs);background:var(--input-dot);max-width:calc(100% - 2 * var(--sp-3))}
.pd-buy-v{position:relative;font-weight:600}
/* The PO button names what the file holds before it downloads. */
.pd-po{white-space:nowrap}
.pd-po-n{font-weight:400;color:var(--ink3)}
@media (max-width:640px){.pd-row{grid-template-columns:24px minmax(0,1fr) 52px}.pd-row > :nth-child(3),.pd-row > :nth-child(4),.pd-row > :nth-child(6){display:none}}
/* A summary card that links (x-stat href): the whole card is the target. */
.stat-link{color:inherit;text-decoration:none;cursor:pointer;transition:box-shadow .15s,border-color .15s}
.stat-link:hover{border-color:var(--accent);box-shadow:0 4px 14px rgba(var(--shadow-rgb),.10)}
.stat-link:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.stat-go{margin-top:var(--sp-2);font-size:var(--fs-xs);font-weight:600;color:var(--accent)}
/* ===== AGING — stock by age, tier chips (8 Oct 2026)
 * The Aging page's bar of stock by the age of its oldest unit (the dashboard's
 * .pd-bar / .pd-legend, six bands from fresh to very old) and the 30+ / 45+ /
 * 60+ chip on a product whose sales are under 10% of SOH. Mobbin: Deel's asset
 * overview bar, Employment Hero's tenure buckets. */
.rs-ages{border-bottom:1px solid var(--line2)}
.ab-1{background:var(--ok-tx)}
.ab-2{background:var(--ok-tx);opacity:.55}
.ab-3{background:var(--price-tx);opacity:.6}
.ab-4{background:var(--price-tx)}
.ab-5{background:var(--urgent-tx);opacity:.65}
.ab-6{background:var(--urgent-tx)}
.rs-table .rs-age{white-space:nowrap}
.rs-age-avg{display:block;font-size:var(--fs-micro);color:var(--ink3);font-weight:400;line-height:1.3}
.rs-tier{display:inline-block;margin-left:6px;padding:1px 7px;border-radius:999px;font-size:var(--fs-micro);font-weight:700;letter-spacing:.02em;background:var(--price-bg);color:var(--price-tx)}
.rs-tier.is-60{background:var(--urgent-bg);color:var(--urgent-tx)}
.rs-tier.rp-up{background:var(--ok-bg);color:var(--ok-tx)}
/* Act today: stock sitting in a warehouse the picker left out — a transfer
 * before a purchase. */
.rs-table td.rs-prod{position:relative}
.rs-menu-btn{position:absolute;top:4px;right:2px}
.rs-table tr:hover .rs-menu-btn{opacity:1}

/* ===== REPRICING (8 Oct 2026)
 * /aging/reprice. Fees & rules: one row per setting — label, the value with
 * its unit, its cap — so the boxes line up down each group (Mobbin:
 * Substack's settings, Xero's grouped form). The proposal: before → new price
 * per tier with a box to type over it (Lookup's .lk-num), the floor and
 * today's margin under it, and the reasoning on a full-width line of its own
 * under the row (Mobbin: Mixpanel's expanded row) rather than a tall narrow
 * column; the pair shares one hover and one rule, so the zebra is off. A
 * greyed pair is listed but not proposed. */
.rp-settings{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:var(--sp-4);padding:var(--sp-4) var(--sp-5);align-items:start}
.rp-stack{display:grid;gap:var(--sp-4)}
.rp-group{border:1px solid var(--line2);border-radius:var(--radius-sm);padding:var(--sp-2) var(--sp-4) var(--sp-3);display:grid;gap:2px;margin:0;min-width:0}
.rp-group legend{font-weight:700;padding:0 var(--sp-1)}
/* The platform's own hue on its fee group and on the price column it feeds
 * (the marketplace picker's tokens; brands, never actions). */
.rp-plat{display:inline-block;border-radius:999px;padding:2px 10px;font-size:var(--fs-sm)}
.rp-group.mk-tokopedia{border-color:var(--plat-olt-tx)}
.rp-group.mk-shopee{border-color:var(--plat-ols-tx)}
.rp-group.mk-tokopedia .rp-plat,.rp-table th.mk-tokopedia{background:var(--plat-olt-bg);color:var(--plat-olt-tx)}
.rp-group.mk-shopee .rp-plat,.rp-table th.mk-shopee{background:var(--plat-ols-bg);color:var(--plat-ols-tx)}
.rp-plat-tag{display:block;font-size:var(--fs-micro);font-weight:600;letter-spacing:.04em;opacity:.85}
.rp-field{display:grid;grid-template-columns:minmax(0,1fr) 112px 112px;gap:var(--sp-3);align-items:center;min-height:34px;font-size:var(--fs-sm)}
.rp-field label{color:var(--ink2)}
.rp-field-head{min-height:0;font-size:var(--fs-micro);font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--ink3);margin-top:var(--sp-1)}
.rp-in{display:inline-flex;align-items:center;gap:var(--sp-1);min-width:0}
.rp-in input,.rp-in select{width:68px;padding:5px var(--sp-2);border:1px solid var(--line);border-radius:var(--radius-xs);font:inherit;color:var(--ink);background:var(--control-bg);font-variant-numeric:tabular-nums}
.rp-in select{width:auto}
.rp-in .hint{margin:0;white-space:nowrap}
.rp-in input[type=checkbox]{width:auto;padding:0}
.rp-in.rp-wide{grid-column:span 2}
.rp-in.rp-wide select{width:100%}
.rp-group > .hint{margin:var(--sp-2) 0 0}
.rp-unit{color:var(--ink3);font-size:var(--fs-xs);white-space:nowrap}
.rp-table .rp-why{white-space:nowrap;font-size:var(--fs-sm)}
.rp-table .rp-price{white-space:nowrap}
.rp-change{display:inline-flex;align-items:center;gap:2px}
.rp-table .rp-new input{width:84px;font-weight:700}
.rp-arrow{color:var(--ink3);margin:0 2px}
/* The quiet second line of a cell (why, and margin before → after · floor). */
.rp-sub{display:block;font-size:var(--fs-micro);color:var(--ink3);font-weight:400;line-height:1.3;white-space:normal}
/* The fee total carries the fee-by-fee detail in its tooltip. */
.rp-fees{text-decoration:underline dotted;text-underline-offset:3px;cursor:help}
.rp-whynot{display:block;font-size:var(--fs-micro);color:var(--urgent-tx);font-weight:600;line-height:1.3;white-space:normal}
/* A product is two rows: the figures, then its reasoning. One hover, one
 * rule under the pair, no stripes. */
.rs-table.rp-table tbody tr:nth-child(even) > td{background:transparent}
.rs-table.rp-table tbody tr.rp-main > td{padding-bottom:2px}
.rs-table.rp-table tbody tr.rp-reason-row > td{padding-top:0;padding-bottom:9px;border-bottom:1px solid var(--line2)}
.rs-table.rp-table tbody tr:hover > td{background:transparent}
.rs-table.rp-table tbody tr.rp-main:hover > td,.rs-table.rp-table tbody tr.rp-main:hover + tr.rp-reason-row > td{background:var(--row-hover)}
.rp-reason{font-size:var(--fs-sm);color:var(--ink2);white-space:normal;max-width:none;line-height:1.45}
.rs-table tbody tr.rp-skip > td{opacity:.55}
.rs-table tbody tr.rp-out > td{background:var(--inset)}
.rp-filters{display:flex;flex-wrap:wrap;gap:var(--sp-2);align-items:center;padding:0 var(--sp-5) var(--sp-3)}
.rp-table .rp-tax{white-space:nowrap;font-size:var(--fs-sm);color:var(--ink2)}
/* A long name ends in an ellipsis inside its cell rather than running under Category. */
.rp-table .rs-prod .rs-one{display:block;max-width:360px}
.rp-table.rs-sized .rs-prod .rs-one{max-width:none}
.rp-batches{list-style:none;margin:0;padding:var(--sp-2) var(--sp-5) var(--sp-4);display:grid;gap:var(--sp-1)}
.rp-batches li{display:flex;gap:var(--sp-3);align-items:baseline;flex-wrap:wrap}
@media (max-width:900px){.rp-field{grid-template-columns:minmax(0,1fr) 96px 96px}}

/* ===== PC LIST — PC builds by brand and the shops they sell in (9 Oct 2026;
 * redesigned 10 Oct 2026). The brands are a list on the left and the picked
 * brand is the detail on the right (Klaviyo's Groups, Jobber's settings
 * list); where it sells is chosen in a side drawer with a row per division,
 * "N of 28 shops" and a whole-division tick (Shopify's shipping zones,
 * Pinterest's location picker). Selection is the accent wash, never a status
 * hue; the work counts borrow the action inks. */
.pcl{display:grid;gap:var(--sp-4)}
.pcl-bar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2) var(--sp-4)}
.pcl-bar .search{width:min(100%,360px)}
.pcl-fresh{font-size:var(--fs-sm);color:var(--ink2)}
.pcl-empty{padding:var(--sp-5)}
.pcl-empty p{margin:0}
.pcl-split{display:grid;grid-template-columns:minmax(210px,256px) minmax(0,1fr);gap:var(--sp-4);align-items:start}

/* The brand list. */
.pcl-brands{position:sticky;top:var(--sp-4);min-width:0;padding:var(--sp-2)}
.pcl-brands ul{list-style:none;margin:0;padding:0;display:grid;gap:2px}
.pcl-brand{display:grid;gap:3px;width:100%;padding:10px var(--sp-3);border:0;border-radius:var(--radius-sm);background:none;color:var(--ink);font:inherit;text-align:left;cursor:pointer;transition:background-color .15s ease}
.pcl-brand:hover{background:var(--row-hover)}
.pcl-brand.on{background:var(--accent-soft)}
.pcl-brand:focus-visible{outline:none;box-shadow:var(--focus-ring)}
.pcl-brand.is-dim{opacity:.5}
.pcl-brand-top{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-2)}
.pcl-brand-name{font-weight:700;font-size:var(--fs-sm);line-height:1.3}
.pcl-brand.on .pcl-brand-name{color:var(--accent)}
.pcl-brand-n{font-size:var(--fs-xs);font-weight:600;color:var(--ink3);font-variant-numeric:tabular-nums}
.pcl-brand-where{font-size:var(--fs-xs);color:var(--ink2);line-height:1.35}
.pcl-brand-where.is-none{color:var(--ink3)}
.pcl-brand-todo{font-size:var(--fs-xs);font-weight:600;color:var(--urgent-tx);font-variant-numeric:tabular-nums}

/* The picked brand. */
.pcl-detail{min-width:0}
.pcl-head{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--sp-4);padding:var(--sp-5) var(--sp-5) var(--sp-4)}
.pcl-head-text{min-width:0}
.pcl-head h2{margin:0;font-size:var(--fs-lg);font-weight:800;line-height:1.15;letter-spacing:-.01em;overflow-wrap:anywhere}
.pcl-sub{margin:4px 0 0;font-size:var(--fs-sm);color:var(--ink2);font-variant-numeric:tabular-nums}
.pcl-alert{margin:0 var(--sp-5) var(--sp-4)}
.pcl-facts{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,1fr);gap:var(--sp-3) var(--sp-6);margin:0;padding:var(--sp-4) var(--sp-5);border-block:1px solid var(--line2)}
.pcl-fact{display:grid;gap:6px;align-content:start;min-width:0}
.pcl-fact dt{font-size:var(--fs-micro);font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--ink3)}
.pcl-fact dd{margin:0;display:flex;flex-wrap:wrap;align-items:center;gap:4px 6px}
.pcl-tally{gap:4px var(--sp-4) !important}
.pcl-none{font-size:var(--fs-sm);color:var(--ink3)}
.pcl-n{font-size:var(--fs-sm);color:var(--ink2);white-space:nowrap}
.pcl-n b{font-weight:700;color:var(--ink);font-variant-numeric:tabular-nums}
.pcl-n.is-ok b{color:var(--ok-tx)}
.pcl-n.is-input b{color:var(--urgent-tx)}
.pcl-n.is-price b{color:var(--price-tx)}

/* Show: All / Set on their own / Still to input / Base models. */
.pcl-filters{display:flex;flex-wrap:wrap;gap:var(--sp-2);padding:var(--sp-4) var(--sp-5) var(--sp-3)}
.pcl-filter{display:inline-flex;align-items:center;gap:6px;padding:5px 12px;border:1px solid var(--line);border-radius:999px;background:var(--control-bg);color:var(--ink2);font:inherit;font-size:var(--fs-sm);cursor:pointer;transition:border-color .15s ease,background-color .15s ease,color .15s ease}
.pcl-filter:hover{border-color:var(--ink3);color:var(--ink)}
.pcl-filter.on{background:var(--accent-soft);border-color:var(--accent-line);color:var(--ink);font-weight:600}
.pcl-filter:focus-visible{outline:none;box-shadow:var(--focus-ring)}
.pcl-filter-n{color:var(--ink3);font-variant-numeric:tabular-nums}
.pcl-filter.on .pcl-filter-n{color:var(--accent)}

/* The builds. */
.pcl-table{width:100%}
/* The name takes the free width and wraps to two lines (owner, 10 Oct 2026:
 * "the name column width is so small to the point the names cannot be
 * seen"); the full name is its hover title. Sized by a grip, it follows the
 * column instead. */
.pcl-table .rs-prod{min-width:300px;max-width:none}
.pcl-table .rs-one{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;max-width:none;white-space:normal;overflow:hidden;line-height:1.35}
.pcl-table td.num,.pcl-table th.num{width:1%;white-space:nowrap}
.pcl-table:not(.rs-sized) th[data-col=pc_build]{width:48%}
.pcl-own{display:block;margin-top:2px;font-size:var(--fs-xs);color:var(--ink3)}
.pcl-sold{min-width:150px}
/* Its counts wrap one per line rather than widen the column past the names. */
.pcl-progress .pcl-n{display:inline-block;margin-right:var(--sp-3)}
.pcl-acts{text-align:right;min-width:88px}
.pcl-acts > *{white-space:nowrap}
.pcl-nowhere{display:inline-block;padding:1px 8px;border-radius:999px;background:var(--urgent-bg);color:var(--urgent-tx);font-size:var(--fs-sm);font-weight:600}
.pcl-table-empty{margin:0;padding:var(--sp-2) var(--sp-5) var(--sp-5)}
.pcl-pager{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--sp-3);padding:0 var(--sp-5) var(--sp-3)}
.pcl-foot{margin:0;max-width:96ch}
/* One place a build sells (the _pc-scope partial). */
.pc-chip{display:inline-block;padding:1px 8px;border-radius:999px;background:var(--line2);color:var(--ink2);font-size:var(--fs-sm);white-space:nowrap;margin:1px 2px 1px 0}
.pc-chip.is-div{background:var(--accent-soft);color:var(--ink)}

/* The drawer: where it sells. */
.pcl-drawer{width:520px}
.pcl-pick-head{flex:1;min-width:0}
.pcl-pick-head h2{margin:0}
.pcl-close{display:grid;place-items:center;flex:none;width:32px;height:32px;margin:-4px -6px 0 0;padding:0;border:0;border-radius:var(--radius-sm);background:none;color:var(--ink2);cursor:pointer}
.pcl-close svg{width:18px;height:18px}
.pcl-close:hover{background:var(--hover-strong);color:var(--ink)}
.pcl-close:focus-visible{outline:none;box-shadow:var(--focus-ring)}
.pcl-pick-body{display:grid;gap:var(--sp-3);align-content:start}
.pcl-pick-search{width:100%}
.pcl-nowhere-opt{display:flex;align-items:center;gap:var(--sp-2);padding:var(--sp-3);border:1px solid var(--line);border-radius:var(--radius-sm);font-size:var(--fs-sm);cursor:pointer}
.pcl-nowhere-opt:hover{background:var(--row-hover)}
.pcl-div{border:1px solid var(--line);border-radius:var(--radius-sm);overflow:hidden}
.pcl-div-head{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);padding:10px var(--sp-3);background:var(--inset)}
.pcl-div-whole{display:flex;align-items:center;gap:var(--sp-2);cursor:pointer;font-size:var(--fs-sm)}
.pcl-div-note{font-size:var(--fs-xs);color:var(--ink2);font-weight:400}
.pcl-div-count{font-size:var(--fs-xs);font-weight:600;color:var(--ink2);font-variant-numeric:tabular-nums;white-space:nowrap}
.pcl-div-covered{margin:0;padding:10px var(--sp-3);border-top:1px solid var(--line2);font-size:var(--fs-sm);color:var(--ink2)}
.pcl-div-covered[x-cloak]{display:none}
.pcl-shops{display:grid;grid-template-columns:repeat(auto-fill,minmax(138px,1fr));gap:2px var(--sp-2);padding:var(--sp-2);border-top:1px solid var(--line2)}
.pcl-shop{display:flex;align-items:center;gap:6px;padding:5px 6px;border-radius:var(--radius-xs);font-family:var(--font-mono);font-size:var(--fs-xs);font-weight:600;color:var(--ink);cursor:pointer}
.pcl-shop:hover{background:var(--row-hover)}
.pcl-pick-sum{margin-left:auto;font-size:var(--fs-sm);font-weight:600;color:var(--ink)}
.pcl-pick-hint{flex-basis:100%;margin:0;font-size:var(--fs-xs);color:var(--ink2)}
@media (max-width:900px){
  .pcl-split{grid-template-columns:minmax(0,1fr)}
  .pcl-brands{position:static}
  .pcl-brands ul{display:flex;overflow-x:auto;gap:var(--sp-2)}
  .pcl-brand{min-width:180px}
  .pcl-facts{grid-template-columns:minmax(0,1fr)}
}
@media (prefers-reduced-motion:reduce){.pcl-brand,.pcl-filter{transition:none}}

/* ===== STORE PRICING — which shops have their prices checked (10 Oct 2026)
 * One row per shop: identity, today's effect, and a real switch with its
 * state in words beside it (Mobbin: Airwallex, Clay, Salesforce). The ON
 * track is the accent; OFF is the neutral line — never a status hue, since
 * "own pricing" is a setting, not a problem. */
.sp{display:grid;gap:var(--sp-4)}
.sp-intro{margin:0;max-width:80ch}
.sp-search{padding:0 var(--sp-5) var(--sp-4)}
.sp-search .search{width:min(100%,320px)}
.sp-list{list-style:none;margin:0;padding:0}
.sp-row{display:grid;grid-template-columns:minmax(150px,220px) minmax(0,1fr) auto;align-items:center;gap:var(--sp-2) var(--sp-5);padding:var(--sp-3) var(--sp-5);border-top:1px solid var(--line2)}
.sp-row:first-child{border-top:0}
.sp-row:hover{background:var(--row-hover)}
.sp-id{display:grid;gap:2px}
.sp-effect{min-width:0;font-size:var(--fs-sm)}
.sp-control{display:flex;align-items:center;gap:var(--sp-3)}
.sp-state{font-size:var(--fs-sm);font-weight:600;color:var(--ink2);white-space:nowrap}
.sp-row.is-own .sp-state{color:var(--ink)}
.sp-switch{position:relative;flex:none;width:40px;height:22px;padding:0;border:0;border-radius:999px;background:var(--line);cursor:pointer;transition:background .15s ease}
.sp-switch.on{background:var(--accent)}
.sp-knob{position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:var(--panel);box-shadow:0 1px 2px rgba(var(--shadow-rgb),.3);transition:transform .15s cubic-bezier(.2,.8,.2,1)}
.sp-switch.on .sp-knob{transform:translateX(18px)}
.sp-switch:focus-visible{outline:none;box-shadow:var(--focus-ring)}
.sp-switch:disabled{opacity:.6;cursor:progress}
.sp-msg{grid-column:1 / -1;margin:0}
@media (max-width:720px){
  .sp-row{grid-template-columns:1fr auto}
  .sp-effect{grid-column:1 / -1;grid-row:2}
}
@media (prefers-reduced-motion:reduce){.sp-switch,.sp-knob{transition:none}}

/* ===== INPUT DEADLINE — the 7-day countdown and its penalties (10 Oct 2026)
 * The dashboard line and the /penalties report. Overdue reuses the urgent
 * pair and "due soon" the price amber, as the worklist's Deadline pills do;
 * the words always carry the fact, the colour only its loudness. */
.deadline-rows{display:grid;gap:var(--sp-3)}
.deadline-line{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--sp-3) var(--sp-6)}
.deadline-kind{min-width:120px;font-weight:600;color:var(--ink)}
.deadline-stat{color:var(--ink2);text-decoration:none}
.deadline-stat b{color:var(--ink);font-variant-numeric:tabular-nums}
.deadline-stat.is-over,.deadline-stat.is-over b{color:var(--urgent-tx)}
.deadline-stat.is-soon,.deadline-stat.is-soon b{color:var(--price-tx)}
/* /penalties (redesign, 10 Oct 2026). The month reads as a STATEMENT — one
 * sentence with its amount set large inline (Etsy's monthly statement,
 * PlanetScale's invoice) — not a row of KPI cards; the queue's count tabs
 * are filters with their cost on them (Stripe, Vanta). Selected state is the
 * accent wash, never urgent red (red means overdue money). */
.pen{display:grid;gap:var(--sp-4)}
.pen > .alert{margin:0}
.pen-statement{padding:var(--sp-5) var(--sp-6) var(--sp-6)}
.pen-statement-bar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-3) var(--sp-4)}
.pen-stepper{display:inline-flex;align-items:stretch;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--control-bg);overflow:hidden}
.pen-step{display:grid;place-items:center;width:36px;min-height:36px;border:0;background:transparent;color:var(--ink2);cursor:pointer;transition:background .15s ease,color .15s ease}
.pen-step svg{width:16px;height:16px}
.pen-step:hover:not(:disabled){background:var(--hover-strong);color:var(--accent)}
.pen-step:disabled{color:var(--ink3);opacity:.35;cursor:default}
.pen-step:focus-visible,.pen-month:focus-visible,.pen-tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.pen-month{appearance:none;-webkit-appearance:none;border:0;border-inline:1px solid var(--line);background:transparent;font:inherit;font-weight:700;font-size:var(--fs-base);color:var(--ink);padding:0 var(--sp-4);text-align:center;cursor:pointer}
.pen-rule{margin:0 0 0 auto;font-size:var(--fs-sm);color:var(--ink2)}
.pen-rule b{color:var(--ink);font-variant-numeric:tabular-nums}
.pen-sentence{margin:var(--sp-6) 0 0;font-size:var(--fs-lg);font-weight:600;line-height:1.3;color:var(--ink2);letter-spacing:-.01em;text-wrap:balance;max-width:34em}
.pen-amount{display:inline-block;margin:0 .08em;font-size:var(--fs-hero);font-weight:800;line-height:1;letter-spacing:-.03em;color:var(--urgent-tx);font-variant-numeric:tabular-nums;vertical-align:-.06em}
.pen-amount.is-zero{color:var(--ok-tx)}
.pen-ledger{display:flex;flex-wrap:wrap;gap:var(--sp-3) var(--sp-10);margin:var(--sp-5) 0 0;padding-top:var(--sp-4);border-top:1px solid var(--line2)}
.pen-ledger > div{display:grid;gap:3px}
.pen-ledger dt{font-size:var(--fs-micro);font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--ink3)}
.pen-ledger dd{margin:0;font-size:var(--fs-md);font-weight:700;color:var(--ink);font-variant-numeric:tabular-nums}
.pen-ledger .is-pending dd{color:var(--price-tx)}
.pen-ledger .is-done dd{color:var(--ok-tx)}
.pen-note{margin:var(--sp-4) 0 0;font-size:var(--fs-sm);color:var(--ink2);max-width:75ch}
.pen-tabs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));border-bottom:1px solid var(--line)}
.pen-tab{position:relative;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:baseline;gap:2px var(--sp-3);padding:var(--sp-4) var(--sp-5);border:0;border-right:1px solid var(--line2);background:var(--panel);font:inherit;text-align:left;color:var(--ink2);cursor:pointer;transition:background .15s ease}
.pen-tab:last-child{border-right:0}
.pen-tab:hover{background:var(--row-hover)}
.pen-tab[aria-pressed="true"]{background:var(--accent-soft)}
.pen-tab[aria-pressed="true"]::after{content:"";position:absolute;inset:auto 0 -1px 0;height:2px;background:var(--accent)}
.pen-tab-label{font-size:var(--fs-sm);font-weight:600;color:var(--ink)}
.pen-tab-n{grid-row:1 / span 2;grid-column:2;align-self:center;font-size:var(--fs-xl);font-weight:800;line-height:1;color:var(--ink);font-variant-numeric:tabular-nums}
.pen-tab-cost{font-size:var(--fs-xs);color:var(--ink3)}
.pen-tab.is-over .pen-tab-n,.pen-tab.is-over .pen-tab-cost{color:var(--urgent-tx)}
.pen-tab.is-soon .pen-tab-n,.pen-tab.is-soon .pen-tab-cost{color:var(--price-tx)}
.pen-filters{display:flex;flex-wrap:wrap;gap:var(--sp-2);padding:var(--sp-3) var(--sp-5);border-bottom:1px solid var(--line2);background:var(--inset)}
.pen .spec .meta-line{display:block;margin-top:2px}
.pen-group th{padding:var(--sp-2) var(--sp-3);text-align:left;font-size:var(--fs-xs);font-weight:700;letter-spacing:.03em;color:var(--ink2);background:var(--inset);border-bottom:1px solid var(--line2)}
.pen tfoot th,.pen tfoot td{padding:10px var(--sp-3);border-top:1px solid var(--line);text-align:left}
.pen tfoot td.num{text-align:right}
.pen-settings > summary{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--sp-3);padding:var(--sp-4) var(--sp-5);cursor:pointer;list-style:none}
.pen-settings > summary::-webkit-details-marker{display:none}
.pen-settings > summary::before{content:"";flex:none;align-self:center;width:7px;height:7px;border-right:2px solid var(--ink3);border-bottom:2px solid var(--ink3);transform:rotate(-45deg);transition:transform .15s ease}
.pen-settings[open] > summary::before{transform:rotate(45deg)}
.pen-settings[open] > summary{border-bottom:1px solid var(--line)}
.pen-settings > summary:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.pen-settings-title{font-size:var(--fs-md);font-weight:700;color:var(--ink)}
.pen-settings .addbar{border-bottom:0}
.pen-settings-hint{margin:0;padding:0 var(--sp-5) var(--sp-4)}
@media (max-width:720px){
  .pen-statement{padding:var(--sp-4)}
  .pen-rule{margin-left:0;flex-basis:100%}
  .pen-tabs{grid-template-columns:1fr}
  .pen-tab{border-right:0;border-bottom:1px solid var(--line2)}
  .pen-tab:last-child{border-bottom:0}
  .pen-amount{font-size:var(--fs-xl)}
}
@media (prefers-reduced-motion:reduce){.pen-step,.pen-tab,.pen-settings > summary::before{transition:none}}

/* ===== PRICE GAP RANKING — the store dashboard's announcement (2 Oct 2026)
 *
 * Owner: "just the announcement of worst shop ranking is good", then "it
 * needs to look bad and even shameful". In the BOTTOM zone (the worst third,
 * Dashboard::gapAlarm()) a SOLID red bar, white ink, caution-tape edges and a
 * league strip with this shop pinned YOU (Mobbin: Etsy's deletion bar,
 * Duolingo's league zones). --shame-bg / --shame-tx are their own per-theme
 * pair, measured by PaletteContrastTest; --on-accent is NOT used here (it is
 * ink for the accent fill only). Outside the zone only .gap-note, one quiet
 * line (owner: "there's no need to show them something so strong"). The
 * pulse is a box-shadow, never a transform (the .content trap), and off for
 * reduced motion.
 */
.gap-banner{margin-bottom:var(--sp-4);border-radius:var(--radius);overflow:hidden;background:var(--shame-bg);color:var(--shame-tx)}
/* The division table's Price gaps column (owner, 6 Oct 2026): a quiet rank,
 * and for the bottom zone the same urgent red the shop's own banner wears. */
.dd-gap{color:var(--ink);text-decoration:none;font-weight:600;font-variant-numeric:tabular-nums}
.dd-gap:hover{text-decoration:underline}
.dd-gap.is-critical{color:var(--urgent-tx);font-weight:800}
.dd-gap-tag{display:inline-block;margin-left:var(--sp-1);padding:0 6px;border-radius:999px;background:var(--urgent-bg);box-shadow:inset 0 0 0 1px var(--urgent-line);color:var(--urgent-tx);font-size:var(--fs-micro);font-weight:800;letter-spacing:.04em;text-transform:uppercase}
.gap-note{margin:0 0 var(--sp-4);font-size:var(--fs-sm);color:var(--ink2)}
.gap-tape{height:12px;background:repeating-linear-gradient(-45deg,var(--shame-tape-a) 0 14px,var(--shame-tape-b) 14px 28px)}
.gap-banner-body{display:flex;align-items:center;gap:var(--sp-5);padding:var(--sp-4) var(--sp-5)}
.gap-rank-badge{flex:none;min-width:96px;height:96px;padding:0 var(--sp-2);border-radius:50%;display:flex;align-items:center;justify-content:center;
  font-size:46px;font-weight:900;line-height:1;font-variant-numeric:tabular-nums;border:4px solid currentColor}
.gap-rank-hash{font-size:22px;font-weight:800;align-self:flex-start;margin-top:24px;opacity:.85}
.gap-banner-text{flex:1 1 auto;min-width:0}
.gap-banner-label{display:inline-block;font-size:var(--fs-xs);font-weight:900;letter-spacing:.14em;padding:2px 10px;border-radius:4px;
  border:2px solid currentColor;margin-bottom:var(--sp-2)}
.gap-banner-h{font-size:22px;font-weight:850;line-height:1.2;text-wrap:balance}
.gap-worst{text-transform:uppercase;text-decoration:underline;text-decoration-thickness:3px;text-underline-offset:4px}
.gap-banner-sub{font-size:var(--fs-sm);margin-top:var(--sp-1)}
.gap-banner-btn{flex:none;white-space:nowrap;text-decoration:none;font-weight:800;font-size:var(--fs-base);
  padding:12px var(--sp-5);border-radius:8px;background:var(--shame-tx);color:var(--shame-bg)}
.gap-banner-btn:hover{filter:brightness(1.08)}
/* The league strip: one slot per shop, best on the left. */
.gap-league{display:flex;align-items:center;gap:var(--sp-2);margin-top:var(--sp-4);padding-top:18px;font-size:var(--fs-micro);font-weight:700;text-transform:uppercase;letter-spacing:.06em}
.gap-league-slots{display:flex;gap:4px;flex:1 1 auto;min-width:0}
.gap-slot{position:relative;flex:1 1 0;height:12px;border-radius:4px;border:1.5px solid currentColor;opacity:.45}
.gap-slot.in-zone{opacity:1;background:currentColor}
.gap-slot.zone-start{margin-left:6px}
.gap-slot.is-you{opacity:1;height:22px;margin-top:-5px;background:currentColor;box-shadow:0 0 0 3px var(--shame-tape-a)}
.gap-zone-tag{position:absolute;bottom:calc(100% + 4px);left:0;white-space:nowrap;font-weight:900}
.gap-you{position:absolute;top:calc(100% + 4px);left:50%;translate:-50% 0;white-space:nowrap;font-weight:900;font-size:var(--fs-xs)}
.gap-league{padding-bottom:18px}
.gap-tape-thin{height:6px}
/* The bar that follows staff on every page (layouts.app): the same shame
 * colours, one line per shop, sticky at the top of the scroll. */
.gap-sticky{position:sticky;top:0;z-index:21;background:var(--shame-bg);color:var(--shame-tx);box-shadow:0 4px 14px rgba(var(--shadow-rgb),.25)}
.gap-sticky-row{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-2) var(--sp-6);font-size:var(--fs-sm)}
.gap-sticky-row + .gap-sticky-row{border-top:1px dashed currentColor}
.gap-sticky-tag{flex:none;font-size:var(--fs-micro);font-weight:900;letter-spacing:.12em;padding:2px 8px;border:2px solid currentColor;border-radius:4px}
.gap-sticky-text{flex:1 1 auto;min-width:0}
.gap-sticky-rank{font-size:18px;font-weight:900;margin-right:4px}
.gap-sticky-btn{padding:6px var(--sp-4);font-size:var(--fs-sm)}
@media (max-width:700px){.gap-sticky-row{flex-wrap:wrap;padding:var(--sp-2) var(--sp-4)}}
@keyframes gap-pulse{0%,100%{box-shadow:0 0 0 0 rgba(var(--shadow-rgb),0)}50%{box-shadow:0 0 0 8px var(--urgent-line)}}
.gap-banner{animation:gap-pulse 2.2s ease-in-out infinite}
@media (prefers-reduced-motion: reduce){.gap-banner{animation:none}}
@media (max-width:700px){.gap-banner-body{flex-wrap:wrap}.gap-rank-badge{min-width:64px;height:64px;font-size:30px}.gap-banner-h{font-size:18px}}

/* ================================================== NEW STORES
 * /new-stores (owner, 4 Oct 2026): one panel per shop the import found, with
 * its setup as a two-step checklist and one Register press. Done steps take
 * the ok pair; to-do steps stay neutral, because waiting is not a fault. */
.ns-list > .panel + .panel{margin-top:var(--sp-4)}
.ns-bar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-3);margin-bottom:var(--sp-4)}
.ns-bar .hint{margin:0}
.ns-steps{list-style:none;margin:0;padding:var(--sp-1) var(--sp-5)}
.ns-step{display:flex;gap:var(--sp-3);padding:var(--sp-3) 0;border-top:1px solid var(--line2)}
.ns-step:first-child{border-top:0}
.ns-mark{flex:0 0 22px;height:22px;border-radius:50%;display:grid;place-items:center;font-size:var(--fs-xs);font-weight:700;border:1.5px solid var(--line);color:var(--ink3)}
.ns-step.is-done .ns-mark{background:var(--ok-bg);border-color:var(--ok-line);color:var(--ok-tx)}
.ns-step-body{flex:1;min-width:0;font-size:var(--fs-sm);color:var(--ink2)}
.ns-step-h{font-weight:650;color:var(--ink);margin-bottom:var(--sp-1)}
.ns-wh{border:0;margin:var(--sp-2) 0 0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:var(--sp-1) var(--sp-4)}
.ns-wh-row{display:flex;align-items:center;gap:var(--sp-2)}
.ns-region{margin-top:var(--sp-2);border-top:1px solid var(--line2);padding-top:var(--sp-2)}
.ns-region > summary{cursor:pointer;font-size:var(--fs-sm);color:var(--ink)}
.ns-region > summary > * + *{margin-left:var(--sp-2)}
.ns-region > summary:focus-visible{box-shadow:var(--focus-ring);border-radius:var(--radius-xs)}
.ns-confirm{display:flex;align-items:center;gap:var(--sp-2);font-size:var(--fs-sm);color:var(--ink)}
.ns-foot{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-3);padding:var(--sp-3) var(--sp-5);border-top:1px solid var(--line2)}
.ns-foot .alert{flex-basis:100%;margin:0}
.ns-foot .hint{margin:0}

/* ================================================== PRODUCT LOOKUP
 * Purchase › Product lookup (5 Oct 2026). The table is Order Forecast's
 * .rs-table; each pasted line is a heading row (tbody th) above its matches,
 * so the columns line up across every group. */
.lk-actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-3);margin-top:var(--sp-3)}
.lk-actions .hint{margin:0}
.lk-results{margin-top:var(--sp-4)}
.rs-table .lk-line th{text-align:left;padding:var(--sp-3) var(--sp-3) var(--sp-2);background:var(--panel);border-top:1px solid var(--line);font-size:var(--fs-sm);font-weight:500;color:var(--ink2)}
.rs-table tbody:first-of-type .lk-line th{border-top:0}
.lk-q{display:inline-block;padding:2px var(--sp-3);border-radius:999px;background:var(--brand-fill);color:var(--on-accent);font-weight:700;margin-right:var(--sp-2)}
/* The deal terms, one labelled chip per term (owner, 5 Oct 2026). QTY is
 * neutral so units never read as money; NET, the result, is the rose one. */
.lk-chips{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--sp-1);vertical-align:middle}
.lk-chip{display:inline-flex;align-items:baseline;gap:var(--sp-1);padding:2px var(--sp-2);border-radius:999px;background:var(--panel);box-shadow:inset 0 0 0 1px var(--line);color:var(--ink);font-size:var(--fs-sm);font-weight:600;font-variant-numeric:tabular-nums}
.lk-chip-k{font-size:var(--fs-micro);font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--ink3)}
.lk-chip-qty{background:var(--inset)}
.lk-chip.is-net{background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--accent-line)}
.lk-chip.is-net b{color:var(--accent);font-size:var(--fs-md);font-weight:800}
.lk-num{display:inline-flex;align-items:center;gap:var(--sp-1)}
.lk-num input{width:72px;padding:4px var(--sp-2);border:1px solid var(--line);border-radius:var(--radius-xs);font:inherit;color:var(--ink);background:var(--control-bg);font-variant-numeric:tabular-nums}
.lk-good{color:var(--ok-tx);font-weight:600}
.lk-fails{color:var(--urgent-tx);font-size:var(--fs-xs)}
/* The figures sit together, each column as wide as its numbers, and the
 * product name takes what is left (owner, 5 Oct 2026: "T7D T14D T30D
 * should be closer"). */
.lk-tight th.num,.lk-tight td.num{width:1%;white-space:nowrap;padding-left:var(--sp-4);padding-right:var(--sp-4)}
.lk-tight .rs-prod{max-width:none}
/* The Offers table in three groups, one colour of the AGRES band each: price
 * (rose), stock & sales (purple), outlook (cyan). The tint is a
 * background-IMAGE so the row striping underneath still shows. */
.lk-grouped .lk-p{background-image:linear-gradient(color-mix(in srgb,var(--brand-a) 5%,transparent),color-mix(in srgb,var(--brand-a) 5%,transparent))}
.lk-grouped .lk-m{background-image:linear-gradient(color-mix(in srgb,var(--brand-b) 5%,transparent),color-mix(in srgb,var(--brand-b) 5%,transparent))}
.lk-grouped .lk-o{background-image:linear-gradient(color-mix(in srgb,var(--brand-c) 7%,transparent),color-mix(in srgb,var(--brand-c) 7%,transparent))}
.lk-grouped .lk-gs{border-left:1px solid var(--line)}
.lk-groups th{border-bottom:0;padding-top:var(--sp-2);padding-bottom:var(--sp-1)}
.lk-groups .lk-g{text-align:center;color:var(--ink);font-weight:700;border-top:3px solid}
.lk-groups .lk-g.lk-p{border-top-color:var(--brand-a)}
.lk-groups .lk-g.lk-m{border-top-color:var(--brand-b)}
.lk-groups .lk-g.lk-o{border-top-color:var(--brand-c)}
/* Negotiating an offer: the Adjust panel under its heading, and the × that
 * takes a product out of the comparison. */
.rs-table .lk-adjust td{background:var(--inset);border-top:1px dashed var(--accent-line);padding:var(--sp-3)}
.lk-adjust-grid,.lk-add{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2) var(--sp-4)}
.rs-table .lk-addrow td{background:var(--panel);padding-top:var(--sp-1);padding-bottom:var(--sp-2)}
.lk-adjust .hint{margin:var(--sp-2) 0 0}
.lk-add-q{width:240px}
.lk-num input.lk-add-q{width:240px}
.lk-add[x-cloak]{display:none}
/* The add-a-product picker: name first, code beneath, a checkbox per row,
 * "N selected · Add N products" (Mobbin: Shopify / Fresha "Add products"). */
.lk-pick[x-cloak]{display:none}
.lk-pick{max-width:860px}
.lk-pick-search input.lk-add-q{width:360px}
.lk-pick-list{list-style:none;margin:var(--sp-2) 0 0;padding:0;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--panel);max-height:360px;overflow:auto}
.lk-pick-list li + li{border-top:1px solid var(--line2)}
.lk-pick-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;align-items:center;gap:var(--sp-3);padding:var(--sp-2) var(--sp-3);cursor:pointer}
.lk-pick-row:hover{background:var(--row-hover)}
.lk-pick-row:has(input:checked){background:var(--accent-soft)}
.lk-pick-name{display:flex;flex-direction:column;min-width:0}
.lk-pick-title{font-size:var(--fs-sm);font-weight:600;color:var(--ink);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.lk-pick-name .kode{font-size:var(--fs-micro);color:var(--ink3)}
.lk-pick-fig{font-size:var(--fs-sm);font-variant-numeric:tabular-nums;white-space:nowrap;color:var(--ink2)}
.lk-pick-foot{display:flex;align-items:center;justify-content:flex-end;gap:var(--sp-3);margin-top:var(--sp-2)}
.lk-sug{font-family:var(--font-mono);font-size:var(--fs-xs)}
.lk-x{margin-left:var(--sp-1);border:0;background:none;color:var(--ink3);font-size:var(--fs-md);line-height:1;cursor:pointer;padding:0 4px;border-radius:var(--radius-xs)}
.lk-x:hover{color:var(--urgent-tx);background:var(--urgent-bg)}
.lk-x:focus-visible{box-shadow:var(--focus-ring)}
/* A deal field the person changed from the offer: the AGRES rose wash, so a
 * negotiated figure is never mistaken for the distributor's own. */
.lk-adjust .lk-num input.is-edited{background:var(--accent-soft);border-color:var(--accent);color:var(--ink)}
.lk-adjust .div-picker.is-edited .div-btn.on{box-shadow:0 0 0 2px var(--accent-line)}
/* Adjust deal / Add a deal / Done: a real button, not a link (owner, 5 Oct 2026). */
.lk-deal-btn{display:inline-flex;align-items:center;gap:var(--sp-1);margin-left:var(--sp-2);vertical-align:middle}
.lk-deal-btn svg{width:14px;height:14px}
.lk-match-best{font-weight:800;color:var(--ink)}
/* PR / NR at the head of a product code: the family a buyer must not mix up. */
.lk-fam{display:inline-block;padding:0 5px;border-radius:var(--radius-xs);font-weight:800;letter-spacing:.04em;box-shadow:0 1px 2px rgba(var(--shadow-rgb),.25)}
.lk-fam.is-pr{background:var(--brand-fill);color:var(--on-accent)}
.lk-fam.is-nr{background:var(--ink);color:var(--panel)}
.lk-tight td.lk-matchpct{padding-right:var(--sp-2)}

/* ================================================== RAIL FOLDS (7 Oct 2026)
 * Owner: the side panel "should be collapsible". Two folds, both remembered
 * per browser by window.agresNav in the layout head and read here off <html>
 * so a page load never flashes:
 *   - a section heading is a button; a folded section's words sit in
 *     data-navfold and its links hide (Klaviyo / DoorDash Merchant's
 *     chevron sections on Mobbin);
 *   - data-rail="mini" narrows the rail to its icons (Microsoft Copilot's
 *     icon rail). Desktop only: below 820px the rail is the drawer. */
button.nav-label{display:flex;align-items:center;gap:var(--sp-2);width:100%;background:none;border:0;font-family:inherit;cursor:pointer;text-align:left;border-radius:var(--radius-xs)}
button.nav-label:hover{color:var(--sidebar-ink)}
.nav-chev{width:12px;height:12px;flex-shrink:0;transition:transform .15s ease}
.nav-sec-end{margin-left:auto;display:inline-flex;align-items:center;gap:6px}
/* A folded section keeps its count on the heading (owner, 7 Oct 2026:
 * "the number should appear on the right side of Review"); open, the link
 * carries it, so the heading's copy hides. */
.nav-sec-pill{display:none;margin-left:0;text-transform:none;letter-spacing:0}
:root[data-navfold~="configuration"] [data-sec="configuration"] .nav-sec-pill,
:root[data-navfold~="review"] [data-sec="review"] .nav-sec-pill,
:root[data-navfold~="admin"] [data-sec="admin"] .nav-sec-pill{display:inline-block}
button.nav-label[aria-expanded="false"] .nav-chev{transform:rotate(-90deg)}
:root[data-navfold~="purchase"] #nav-purchase,:root[data-navfold~="monitoring"] #nav-monitoring,
:root[data-navfold~="configuration"] #nav-configuration,:root[data-navfold~="review"] #nav-review,
:root[data-navfold~="admin"] #nav-admin,:root[data-navfold~="you"] #nav-you{display:none}
.rail .brand{position:relative}
.rail-mini{margin-left:auto;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;flex-shrink:0;border:0;border-radius:var(--radius-xs);background:none;color:var(--sidebar-ink2);cursor:pointer}
.rail-mini:hover{color:var(--sidebar-ink);background:rgba(255,255,255,.06)}
.rail-mini svg{width:16px;height:16px}
@media(min-width:821px){
  :root[data-rail="mini"] .rail{width:60px}
  :root[data-rail="mini"] .rail .brand{flex-direction:column;padding:var(--sp-4) 0 var(--sp-3);gap:var(--sp-2)}
  :root[data-rail="mini"] .rail .brand-name,
  :root[data-rail="mini"] .nav-label,
  :root[data-rail="mini"] .subnav,
  :root[data-rail="mini"] .who-text,
  :root[data-rail="mini"] .who .out,
  :root[data-rail="mini"] .who-gear{display:none}
  :root[data-rail="mini"] .rail-mini{margin-left:0}
  /* Every section shows in the icon rail; a fold is a choice about labels. */
  :root[data-rail="mini"] .nav-items{display:block !important}
  :root[data-rail="mini"] .nav{padding:var(--sp-3) 8px}
  :root[data-rail="mini"] .nav-sec + .nav-sec{border-top:1px solid rgba(255,255,255,.07);margin-top:var(--sp-2);padding-top:var(--sp-2)}
  /* The words stay in the DOM (they are each link's accessible name); only
   * their size goes. The pill becomes a dot on the icon. */
  :root[data-rail="mini"] .nav a{font-size:0;justify-content:center;padding:10px 0;position:relative;gap:0}
  :root[data-rail="mini"] .nav a .ico{width:18px;height:18px}
  :root[data-rail="mini"] .nav a .nav-pill{position:absolute;top:5px;right:7px;width:8px;height:8px;min-width:0;padding:0;font-size:0}
  :root[data-rail="mini"] .rail .who{padding:var(--sp-3) 0;display:flex;justify-content:center}
  :root[data-rail="mini"] a.who-row{margin:0}
}
@media(max-width:820px){.rail-mini{display:none}}

/* ================================================== THEME: COLOURBLIND
 *
 * An OPT-IN alternate palette, chosen on /settings and applied by
 * `data-theme="colorblind"` on <html>. The default theme above is
 * untouched by everything below.
 *
 * WHY IT IS ONLY A TOKEN BLOCK. Every rule in this file already reads
 * `var(--…)`, so overriding the tokens reaches the whole app — 1,500 lines
 * of component CSS need no per-theme variant. Anything that ever gets a
 * literal hex again breaks that property, which is why the platform
 * badges, the completion bars and the login backdrop were converted to
 * tokens before this block was written.
 *
 * WHAT IT IMPLEMENTS: the shadcn/ui system in the owner's DESIGN.md —
 * achromatic surfaces, #e7000b reserved for destructive, Geist, 24px
 * containers / 18px interactive, a three-tone surface stack.
 *
 * *** WHAT IT COSTS, RECORDED SO IT IS A DECISION AND NOT A SURPRISE ***
 * The default theme encodes the ACTION WATERFALL in hue (see this file's
 * header). Monochrome cannot carry that, so in this theme the action pills
 * are a tonal ramp and the completion bars lose their traffic light. Every
 * pill still carries its TEXT LABEL, and the pile bar keeps its legend and
 * role="img" description, so nothing is lost that was not already
 * colour-only — but scanning a worklist by eye is slower here. The owner
 * chose palette-only, knowing that. If it bites, adding a second channel
 * (hatching, a leading glyph) is a change to THIS BLOCK ALONE.
 *
 * THREE DELIBERATE DEVIATIONS FROM DESIGN.md, each an accessibility floor
 * this app already meets and will not give up for a palette:
 *
 *  1. MUTED TEXT is #666, not the doc's #737373. #737373 on the doc's own
 *     #f5f5f5 canvas measures 4.35:1 and fails WCAG AA. #737373 survives
 *     here only for icons and non-text marks, where the threshold is 3:1.
 *  2. THE FOCUS RING is dark, not the doc's "1px solid #e5e5e5". A
 *     #e5e5e5 ring on white is ~1.2:1 — a focus indicator nobody can see
 *     is not a focus indicator, and this app is worked by keyboard.
 *  3. A THIRD INK STEP (#404040) exists, which the doc does not define. Its
 *     ink/ink-soft are #0a0a0a and #171717 — indistinguishable in running
 *     text — and this app's hierarchy needs three real steps because all
 *     three carry data.
 */
:root[data-theme="colorblind"]{
  /* ---- ink: three steps that are actually distinguishable ---- */
  --ink:#0a0a0a;        /* 19.3:1 on canvas */
  --ink2:#404040;       /*  9.7:1 */
  --ink3:#666666;       /*  5.3:1 — muted, but still real data */

  --line:#e5e5e5;
  --line2:#f0f0f0;
  --bg:#f5f5f5;         /* canvas  */
  --panel:#ffffff;      /* paper   */

  /* The doc's "primary action" fill. There is no chromatic accent in this
   * theme, so the accent IS ink — which is the doc's whole thesis: the
   * dark-on-light inversion is the only emphasis it has. */
  --accent:#171717;
  --brand-a:#171717; --brand-b:#525252; --brand-c:#a3a3a3;
  --brand-fill:#171717;
  --brand-glow:rgba(0,0,0,.18);
  --sheen:rgba(255,255,255,.55);
  --av-1:#262626; --av-2:#404040; --av-3:#525252; --av-4:#171717; --av-5:#5c5c5c;
  --medal-1:#d4d4d4; --medal-2:#e5e5e5; --medal-3:#cfcfcf;
  --medal-1-tx:#171717; --medal-2-tx:#171717; --medal-3-tx:#171717;
  --podium-1-bg:#ffffff; --podium-1-line:#171717; --podium-1-glow:rgba(0,0,0,.14);
  --podium-2-bg:#ffffff; --podium-2-line:#a3a3a3; --podium-2-glow:rgba(0,0,0,.08);
  --podium-3-bg:#ffffff; --podium-3-line:#d4d4d4; --podium-3-glow:rgba(0,0,0,.06);
  --podium-1-tx:#ffffff; --podium-1-chip:#171717;
  --brand-mark:#171717; --brand-mark-deep:#404040;
  --accent-hover:#0a0a0a;
  --accent-soft:#f0f0f0;
  --accent-line:#e5e5e5;

  /* Stated rather than inherited, even though the value is the root's.
   * PaletteContrastTest reads --on-accent against --accent inside EACH
   * theme block, so a theme that leaves it out is a theme nobody measured
   * — and white-on-accent is the pairing that carries every primary button
   * in the app. 17.9:1 here. */
  --on-accent:#ffffff;

  /* Surface level 1 — one tonal step off the canvas, no divider needed. */
  --sidebar:#fafafa;
  --sidebar-ink:#404040;
  --sidebar-ink2:#666666;

  /* ---- the action language, as a tonal ramp ----
   * Ordered by URGENCY rather than by the waterfall: darkest is the pile
   * most needing work, lightest is the one needing none. All eight take
   * #171717 text, which clears 12:1 on the darkest of them — the ramp is
   * deliberately confined to the light end so the pills stay quiet next to
   * the table text they sit in. */
  --input-bg:#d4d4d4;    --input-tx:#171717;
  --sec-bg:#dcdcdc;      --sec-tx:#171717;
  --activate-bg:#e3e3e3; --activate-tx:#171717;
  --stock-bg:#e9e9e9;    --stock-tx:#171717;
  --price-bg:#efefef;    --price-tx:#171717;
  --hold-bg:#cccccc;     --hold-tx:#171717;
  --nr-bg:#f5f5f5;       --nr-tx:#404040;
  --ok-bg:#fafafa;       --ok-tx:#404040;

  /* THE ONE CHROMATIC HUE IN THE THEME, and it is spent exactly where the
   * doc says to spend it: BLACKLIST means "never sell this", which is the
   * destructive verdict in this app's vocabulary. Darkened from the doc's
   * #e7000b (4.38:1 on this ground — under AA) to #c40009, which is
   * 5.7:1. */
  --urgent-bg:#f5f5f5;   --urgent-tx:#c40009;
  --shame-bg:#c40009;    --shame-tx:#ffffff;   --shame-tape-a:#f5c400; --shame-tape-b:#161616;
  --map-prim-bg:#d6eaf8;  --map-prim-tx:#003f6b;  --map-prim-edge:#0072b2;
  --map-sec-bg:#fdeac6;   --map-sec-tx:#5e3a00;   --map-sec-edge:#e69f00;
  --map-skip-bg:#e8e8e8;  --map-skip-tx:#2e2e2e;  --map-skip-edge:#7a7a7a;
  --map-black-bg:#f9dccb; --map-black-tx:#6e2900;  --map-black-edge:#d55e00;
  --map-on-bg:#f6dfec;    --map-on-tx:#6b2350;    --map-on-edge:#cc79a7;

  /* Dots are SHAPES, not text grounds — 3:1 is the bar, and these sit on
   * the #fafafa rail. Same urgency order as the pills, inverted: a marker
   * has to be darker than its surround to read at 8px. */
  --input-dot:#404040; --sec-dot:#525252;   --activate-dot:#666666;
  --stock-dot:#737373; --price-dot:#858585; --ok-dot:#a3a3a3;
  --nr-dot:#c4c4c4;    --hold-dot:#2e2e2e;

  /* Shopee and Tokopedia are brands, not actions, so they keep two
   * distinct treatments — solid and mid — rather than collapsing into one
   * badge whose only difference is the word inside it. White clears
   * 5.74:1 on #666 and 17.93:1 on #171717, so both pass AA on their own
   * account — this block was already correct when the default palette was
   * darkened for contrast, and needed no change. (The figure here read
   * "4.7:1" until 10 Sep 2026, which understated it; measured, not
   * estimated, now.) */
  --plat-ols:#171717;
  --plat-olt:#666666;
  --plat-ink:#fff;
  --plat-ols-bg:#171717; --plat-ols-tx:#ffffff;
  --plat-olt-bg:#666666; --plat-olt-tx:#ffffff;

  /* The traffic light goes. A single ink fill against the trough reads as
   * an ordinary progress bar, and the percentage beside it is what
   * actually carries "how good is this". Both gradient stops are the same
   * value because the doc forbids gradients outright. */
  --bar-red-a:#171717;    --bar-red-b:#171717;
  --bar-yellow-a:#171717; --bar-yellow-b:#171717;
  --bar-green-a:#171717;  --bar-green-b:#171717;

  /* The hero panel is light in this theme, so the trough inverts from a
   * white wash to an ink one. */
  --bar-trough:rgba(10,10,10,.08);

  --login-bg:#f5f5f5;

  /* The drill-down depth ramp, achromatic. */
  --depth-1:#ededed;
  --depth-2:#f5f5f5;
  --depth-3:#fafafa;

  /* ---- type ----
   * The doc's scale, mapped onto this app's seven roles. Geist is
   * self-hosted (see the @font-face block below) and falls back to the
   * default stack if the files are ever missing, so a broken font can
   * never leave the app unreadable. */
  --fs-micro:12px;
  --fs-xs:12px;
  --fs-sm:13px;
  --fs-base:14px;
  --fs-md:16px;
  --fs-lg:18px;
  --fs-xl:30px;
  --fs-hero:48px;

  --font:"Geist","Segoe UI Variable Text","Segoe UI",system-ui,-apple-system,sans-serif;

  /* Containers 24px, interactive 18px, nested 10px — the doc's "radius
   * defines hierarchy" rule, and it is strict: nothing in between. */
  --radius:24px;
  --radius-sm:18px;
  --radius-xs:10px;

  /* See deviation 3 in the block comment: a ring you cannot see is not a
   * ring. Achromatic, but dark enough to find. */
  --focus-ring:0 0 0 3px #ffffff,0 0 0 5px #0a0a0a;
  --rail-focus:#0a0a0a;

  /* ---- the surfaces and edges this block used to leave to the root ----
   * (24 Sep 2026.) Inherited, they carried the default theme's tints into a
   * theme whose card on /settings promises no colour coding: pink and green
   * alert edges, amber and blue box borders, and blue-tinted hover and inset
   * greys. The last group used to be patched one selector at a time further
   * down; as tokens they reach every rule that already reads them, including
   * the ones nobody had patched yet. */
  --row-hover:#fafafa;
  --hover-strong:#ededed;
  --sec-hover:#ededed;
  --inset:#fafafa;
  --disabled-bg:#ededed;
  --shadow-rgb:10,10,10;
  --urgent-line:#e5e5e5; --ok-line:#e5e5e5;
  --price-line:#e5e5e5;  --input-line:#e5e5e5;
  /* The one edge that must stay visible INSIDE an urgent fill (the typed
   * confirmation, a blacklisted coverage chip): 3.4:1 on --urgent-bg,
   * achromatic, where the root's pink would be the theme's only tint. */
  --urgent-line-strong:#8a8a8a;

  /* The hero panels become ordinary paper, their figures ink — DESIGN.md's
   * Stat Block is "typographic scale alone — no card chrome", and the green
   * completion numeral goes because this theme has no green. */
  --hero-bg:#fafafa;
  --hero-edge:var(--line);
  --hero-ink:var(--ink);
  --hero-good:var(--ink);
  --hero-label:var(--ink3);
  --hero-num-alt:var(--ink2);
  --hero-faint:var(--ink3);
  --hero-line:var(--line);
}

/* Display sizes get the doc's aggressive negative tracking; it is most of
 * what makes the headline voice read as engineered rather than editorial.
 * Scoped to the theme so the default type scale is untouched. */
:root[data-theme="colorblind"] .hero-num{letter-spacing:-.05em}
:root[data-theme="colorblind"] h1,
:root[data-theme="colorblind"] h2{letter-spacing:-.025em}

/* ---- everything the DEFAULT theme hardcodes for a DARK ground -------
 *
 * Tokens reach most of this file, but not all of it: the rail and the two
 * hero panels were built on a navy ground, so their text is literal white
 * and their hairlines are white alphas. On a #fafafa rail that is white on
 * near-white — invisible. These are the rules that cannot be expressed as a
 * token because the DEFAULT value is correct for a dark surface and the
 * theme value is correct for a light one.
 *
 * Kept as one block, in the order the elements appear down the page, so
 * "what does the theme change beyond tokens" has a single answer. */

/* The rail: navy console -> paper surface. */
:root[data-theme="colorblind"] .rail .brand{border-bottom-color:var(--line)}
:root[data-theme="colorblind"] .rail .brand-name{color:var(--ink)}
:root[data-theme="colorblind"] .nav a:hover{background:#f0f0f0;color:var(--ink)}
:root[data-theme="colorblind"] .rail-mini:hover{background:#f0f0f0;color:var(--ink)}
:root[data-theme="colorblind"][data-rail="mini"] .nav-sec + .nav-sec{border-top-color:var(--line)}
:root[data-theme="colorblind"] .nav a.on{background:#f0f0f0;color:var(--ink)}
:root[data-theme="colorblind"] .nav a.on .nav-pill{background:rgba(10,10,10,.08)}
:root[data-theme="colorblind"] .rail .who{border-top-color:var(--line)}
:root[data-theme="colorblind"] .who .name{color:var(--ink)}
:root[data-theme="colorblind"] .who .out:hover{color:var(--ink)}
:root[data-theme="colorblind"] a.who-row:hover,:root[data-theme="colorblind"] a.who-row.on{background:#f0f0f0}
:root[data-theme="colorblind"] a.who-row:hover .who-gear,:root[data-theme="colorblind"] a.who-row.on .who-gear{color:var(--ink)}

/* The sub-nav spine and its rows — a white-alpha gradient on navy becomes
 * an ink-alpha one on paper. */
:root[data-theme="colorblind"] .subnav::before{
  background:linear-gradient(180deg,
    rgba(10,10,10,.03),
    rgba(10,10,10,.14) 12%,
    rgba(10,10,10,.14) 88%,
    rgba(10,10,10,.03));
}
:root[data-theme="colorblind"] .subnav a:hover{background:#f0f0f0;color:var(--ink)}
:root[data-theme="colorblind"] .subnav a.on{background:#ededed;color:var(--ink)}

/* The two dark panels. DESIGN.md has no dark surfaces and its Stat Block is
 * explicitly "typographic scale alone — no card chrome", so both become
 * ordinary paper and their figures become ink. The green hero numeral goes
 * with them: it was the completion figure's own colour, and this theme has
 * no green. The eleven overrides that did this stood here; since
 * 24 Sep 2026 they are the --hero-* tokens in the block above. */

/* The movement line's five overrides stood here and went with it on
 * 22 Sep 2026 — see the removal note in the STORE HERO section upstream.
 * They were the worked example of what this whole block is for: added after
 * it was written, thought about for the SEMANTIC channel and not the
 * CONTRAST one, and measured in the browser at 1.91:1 on this theme's paper
 * ground. HeroPanelThemeTest keeps that reasoning and still guards every
 * .hero-* rule that remains. */

/* The blue-tinted greys — every hover, inset and disabled fill that reads
 * as neutral in the default theme only because everything around it is
 * cool — were overridden here one selector at a time. They are tokens in
 * the block above since 24 Sep 2026 (--row-hover, --hover-strong,
 * --sec-hover, --inset, --disabled-bg), which reaches the rules this list
 * had never caught up with too. */
/* .catrow.open and .brandwrap used to be overridden here with literal
 * hexes, because both hardcoded a blue-tinted white in the default theme.
 * They are var(--accent-soft) / var(--panel) now and follow the tokens on
 * their own — which is what this whole block is supposed to be. */
:root[data-theme="colorblind"] .brandrow{border-bottom-color:var(--line)}

/* Inputs take the doc's soft fill. The hairline border stays, which the doc
 * drops at rest — a select with no edge on a white card loses its
 * affordance, and this app is a form-heavy console. */
:root[data-theme="colorblind"] .search,
:root[data-theme="colorblind"] .filter,
:root[data-theme="colorblind"] .store-pick,
:root[data-theme="colorblind"] .addbar input,
:root[data-theme="colorblind"] .addbar select{background:#f5f5f5}

/* The doc's Secondary Ghost: a tonal sibling of the filled button rather
 * than an outlined one. */
:root[data-theme="colorblind"] .btn-ghost{background:#f5f5f5;color:var(--ink);border-color:transparent}

/* The signed-out card needs its edge back once the backdrop is no longer
 * dark enough to define it by contrast alone. */
:root[data-theme="colorblind"] .login-card{border:1px solid var(--line)}

/* Cards carry the doc's layered hairline-plus-elevation shadow. */
:root[data-theme="colorblind"] .card,
:root[data-theme="colorblind"] .panel{
  box-shadow:0 0 0 1px rgba(23,23,23,.05),0 1px 3px rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);
}

/* ======================================================== THEME: DARK
 *
 * An OPT-IN dark palette, chosen on /settings and applied by
 * `data-theme="dark"` on <html>. Everything above is untouched by it.
 *
 * WHERE IT COMES FROM: the owner's "Factory" style reference — a terminal
 * war room at midnight. Obsidian #101010 canvas, Carbon Lift #1d1a18
 * surfaces, Bone #eeeeee text, with Signal Orange #ee6018 and Metric Green
 * #a0ca92 as the two functional accents. It is called "Dark" on the
 * settings screen and `dark` in the code, deliberately: this codebase has
 * already paid for one thing having four names, and a design system's own
 * name is lineage, which belongs in a comment like this one rather than in
 * an identifier somebody has to translate.
 *
 * *** WHAT IT KEEPS, WHERE COLOURBLIND GIVES UP *** The reference says two
 * accents and no decorative colour. This app's ACTION WATERFALL is encoded
 * in hue (see this file's header) and that is information, not decoration,
 * so all nine actions keep their own hue here — re-tinted for a dark ground
 * rather than flattened. A worklist scans exactly as it does in Default.
 * That is a deliberate departure from the reference, taken because a
 * palette does not get to cost the app its fastest read.
 *
 * *** THREE THINGS THAT ARE NOT THE PALETTE'S OWN VALUES, AND WHY ***
 *
 *  1. --ink3 is #a39c99, NOT the reference's Warm Granite #8a8380. Granite
 *     measures 4.65:1 on --panel, which passes — and then 4.23:1 on a
 *     hovered row and 3.51:1 on --hover-strong, which do not. This file's
 *     header says --ink3 carries store codes, notes, every table header and
 *     the whole "Correct?" column, and forbids lightening it for looks; the
 *     fix here runs the other way for the same reason. #a39c99 clears
 *     4.5:1 on EVERY ground in this block, worst case 4.84:1. Granite is
 *     still spent, on --sidebar-ink2 and --nr-dot.
 *     *** NO SURFACE IN THIS BLOCK MAY BE LIGHTER THAN #34302d. *** That is
 *     where --ink3 lands on 4.84:1, and a lighter one silently fails AA.
 *
 *  2. --plat-ink is #101010, not white. White on this theme's Shopee tan
 *     measures 2.86:1 and on Metric Green 1.59:1 — both far below the
 *     badge's floor. Dark ink on the same two hues is 6.38:1 and 10.29:1.
 *     Same shape of fix the default theme took on 10 Sep, one level up.
 *
 *  3. --focus-ring is a dark gap plus a bright orange band, not the soft
 *     halo the default theme uses: --accent-soft here is 1.07:1 from the
 *     panel, so the default construction would be a focus indicator nobody
 *     can see. This is the colourblind block's deviation 2, hit again for
 *     the same reason — this app is worked by keyboard.
 *
 * THE ACCENT COLLIDES WITH SHOPEE, AND THAT IS KNOWN. The token comment
 * above argues the accent is blue precisely so it cannot imply a platform.
 * This theme has no blue, and a blue accent here would read as something
 * left behind. Signal Orange is taken knowingly, with the collision blunted
 * two ways: --plat-ols is a lighter tan than the accent rather than the
 * accent itself, and every badge carries its platform's word in dark ink.
 * If it ever misleads somebody, --plat-ols:#fafafa is the exit — Shopee
 * loses its hue, which is what colourblind already does.
 */
:root[data-theme="dark"]{
  /* ---- ink. 14.92:1, 10.65:1 and 6.41:1 on --panel; --ink is 16.40:1 on
   * the --bg canvas behind it ---- */
  --ink:#eeeeee;        /* Bone */
  --ink2:#cfcac7;
  --ink3:#a39c99;       /* see note 1 — NOT Warm Granite */

  --line:#3d3a39;       /* Ash Stroke */
  --line2:#2a2724;
  --bg:#101010;         /* Obsidian Canvas */
  --panel:#1d1a18;      /* Carbon Lift */

  /* The surfaces that INVERT. On a light theme a control well is white and
   * a hover is a pale wash; here the well SINKS below the panel and the
   * hover LIFTS above it. Same tokens, opposite direction — which is the
   * whole reason they are tokens. */
  --control-bg:#16130f;
  --inset:#191614;
  --disabled-bg:#262220;
  --row-hover:#262220;
  --hover-strong:#34302d;
  --sec-hover:#2f2b28;
  --lift:#34302d;

  --accent:#ee6018;        /* Signal Orange */
  --accent-hover:#ff7a33;  /* lighter, not darker — the inversion dark needs */
  --accent-soft:#2f1c11;
  --accent-line:#82431d;

  --on-accent:#101010;               /* 5.73:1 on --accent, 7.32:1 on hover */
  /* The brand fill (active nav row, primary buttons, pills, tabs) is this
   * theme's own orange: its ink is #101010, which on the default theme's
   * rose-purple band measures ~3:1. PaletteContrastTest checks the pairing. */
  --brand-fill:#ee6018;
  --brand-glow:rgba(238,96,24,.28);
  --sheen:rgba(255,255,255,.07);
  /* Every AGRES-look token, stated here even where it matches the default,
   * so this block is a complete answer (AGENTS.md: a new hue needs a
   * counterpart in every theme). The band and the ring keep the brand's own
   * hues on the dark ground; people and medals take the same grounds, all
   * with their own ink. */
  --brand-a:#e11d48; --brand-b:#9333ea; --brand-c:#22d3ee;
  --brand-grad:linear-gradient(90deg,var(--brand-a),var(--brand-b) 60%,var(--brand-c));
  --brand-red:#ed1e27; --brand-mark:#e22a2a; --brand-mark-deep:#c72328;
  --av-1:#be185d; --av-2:#7e22ce; --av-3:#0e7490; --av-4:#3a4a7a; --av-5:#475569;
  --medal-1:linear-gradient(145deg,#fbe38a,#d9a520);
  --medal-2:linear-gradient(145deg,#f1f3f6,#aab2bf);
  --medal-3:linear-gradient(145deg,#f7cfa6,#c98a52);
  --medal-1-tx:#3a2a05; --medal-2-tx:#262c36; --medal-3-tx:#3a1d06;
  --podium-1-bg:linear-gradient(135deg,#3a2f12 0%,#1d1a18 60%); --podium-1-line:#6e5a24; --podium-1-glow:rgba(217,165,32,.18);
  --podium-2-bg:linear-gradient(135deg,#3a3f48 0%,#262628 40%,#1d1a18 70%); --podium-2-line:#6b7280; --podium-2-glow:rgba(0,0,0,.35);
  --podium-3-bg:linear-gradient(135deg,#35261a 0%,#1d1a18 60%); --podium-3-line:#6a4a2e; --podium-3-glow:rgba(0,0,0,.35);
  --podium-1-tx:#1a1300; --podium-1-chip:#e3b94a;
  --on-accent-wash:rgba(16,16,16,.22);

  /* The sub-nav wash composites to #4b2918 over the rail, which is DARK —
   * so its label goes light here while --on-accent goes dark. Two tokens
   * because they are two surfaces; see the note beside --subnav-on. */
  --subnav-on:rgba(238,96,24,.22);
  --subnav-on-tx:#eeeeee;  /* 11.11:1 on the composited wash */
  --subnav-on-ct:#f5cdb4;  /*  8.76:1 */

  --sidebar:#1d1a18;       /* the nav well is Carbon Lift */
  --sidebar-ink:#b8b3b0;   /* Pale Stone   8.34:1 */
  --sidebar-ink2:#8a8380;  /* Warm Granite 4.65:1 — a label, not a data cell */
  --rail-focus:rgba(255,255,255,.65);  /* stated, not inherited: it is the ring on --sidebar above */

  /* ---- the ACTION language, dark-adapted. Same hue families, same
   * waterfall order, and every -tx sits in a narrow 7–9.3:1 band on its own
   * -bg so no pill shouts louder than another — the same construction the
   * default block describes. ---- */
  --input-bg:#16223a;    --input-tx:#a9c5ff;    /* 9.15:1  INPUT PRODUCT */
  --sec-bg:#261c38;      --sec-tx:#cdb2f2;      /* 8.61:1  INPUT PRODUCT (SECONDARY) */
  --activate-bg:#102a35; --activate-tx:#8fd6f0; /* 9.29:1  ACTIVATE LISTING */
  --stock-bg:#381c13;    --stock-tx:#ffab8e;    /* 8.53:1  UPDATE STOCK */
  --price-bg:#32240f;    --price-tx:#f2c078;    /* 9.02:1  UPDATE PRICE */
  --ok-bg:#16271b;       --ok-tx:#a0ca92;       /* 8.48:1  OK — Metric Green */
  --nr-bg:#2a2724;       --nr-tx:#b8b3b0;       /* 7.15:1  NOT REQUIRED — outside the ramp */
  --hold-bg:#0f2a26;     --hold-tx:#7ad9c7;     /* 9.12:1  HELD (LOW STOCK) */
  --urgent-bg:#3a1615;   --urgent-tx:#ff9d96;   /* 8.05:1  refusals and failures */
  --shame-bg:#b3121b;    --shame-tx:#ffffff;   --shame-tape-a:#f5c400; --shame-tape-b:#0b0b0b;
  --map-prim-bg:#12371f;  --map-prim-tx:#a3e9b8;  --map-prim-edge:#22c55e;
  --map-sec-bg:#3d2a0a;   --map-sec-tx:#fcd38a;   --map-sec-edge:#f59e0b;
  --map-skip-bg:#2a2e35;  --map-skip-tx:#cdd3dc;  --map-skip-edge:#8b95a5;
  --map-black-bg:#3d1414; --map-black-tx:#ffb8b8;  --map-black-edge:#ef4444;
  --map-on-bg:#24224d;    --map-on-tx:#c9d0ff;    --map-on-edge:#818cf8;

  --urgent-line:#7a2b28; --ok-line:#2f5c3c;
  --price-line:#6b4c1c;  --input-line:#2c4270;
  --urgent-line-strong:#a04a44;  /* 2.73:1 on --urgent-bg — the confirmation
                                  * well keeps a clearly visible edge */

  /* The marker dots are SHAPES, so the bar is 3:1 — and six of the eight
   * default hues already clear 5:1 on Carbon Lift, so they are kept rather
   * than re-tinted for the sake of it. --nr-dot is the one change: the
   * default's #7c88a0 is a cool blue-grey adrift in a warm palette. */
  --input-dot:#6d94f7; --sec-dot:#a67ddb;   --activate-dot:#48aed8;
  --stock-dot:#e2634a; --price-dot:#e39a3d; --ok-dot:#43ad68;
  --nr-dot:#8a8380;    --hold-dot:#2c9d88;

  /* ---- marketplace badges — see note 2 ---- */
  --plat-ols:#e07b2e;   /* Shopee     6.38:1 on --plat-ink */
  --plat-olt:#a0ca92;   /* Tokopedia 10.29:1 — Metric Green is its own family */
  --plat-ink:#101010;
  --plat-ols-bg:#e07b2e; --plat-ols-tx:#101010;
  --plat-olt-bg:#a0ca92; --plat-olt-tx:#101010;

  /* The traffic light keeps all three stops — unlike colourblind, this
   * theme has hues to spend. Green moves onto Metric Green so the
   * positive-trend colour is one value app-wide. */
  --bar-red-a:#e2634a;    --bar-red-b:#cf4028;
  --bar-yellow-a:#efba43; --bar-yellow-b:#dd9a1c;
  --bar-green-a:#a0ca92;  --bar-green-b:#78ab68;

  /* Already authored as a white wash for the dark hero panel, so it is
   * correct unchanged. Restated so this block is a complete statement. */
  --bar-trough:rgba(255,255,255,.12);

  --login-bg:linear-gradient(135deg,#101010,#1d1a18);

  /* Pure black. A navy shadow on a near-black page is muddy rather than
   * invisible, and the four elements that used a shadow for their EDGE get
   * a hairline below instead. */
  --shadow-rgb:0,0,0;

  /* The drill-down ramp. The default runs TOWARDS the page; inverted here,
   * so the group row is the most lifted and the brand grid sinks back. */
  --depth-1:#2a2624;
  --depth-2:#241f1c;
  --depth-3:#1f1c1a;

  --font:"Geist","Segoe UI Variable Text","Segoe UI",system-ui,-apple-system,sans-serif;
  --font-mono:"Geist Mono","Cascadia Mono",Consolas,"SF Mono",ui-monospace,monospace;

  --focus-ring:0 0 0 3px #101010,0 0 0 5px #ee6018;   /* see note 3 */

  /* The hero panels: navy is cold in a warm palette, so re-grounded rather
   * than re-contrasted; the figures take the theme's own inks. */
  --hero-bg:linear-gradient(180deg,#241f1c,#1d1a18);
  --hero-edge:var(--line);
  --hero-ink:var(--ink);
  --hero-good:var(--ok-tx);
  --hero-label:var(--ink2);
  --hero-num-alt:var(--ink2);
  --hero-faint:var(--ink3);
  --hero-line:var(--line);
}

/* ---- what tokens cannot reach ---------------------------------------
 *
 * Far less than the colourblind block needs, because that one turns a DARK
 * rail LIGHT and this one leaves it dark. What is left is the navy-specific
 * blue in the two hero panels, and the four elements that leaned on a drop
 * shadow for their edge. Same order as the page. */

/* The two hero panels' overrides stood here: they are the --hero-* tokens
 * in the block above since 24 Sep 2026. */

/* The movement line's five overrides stood here too, and went with it on
 * 22 Sep 2026 — see the removal note in the STORE HERO section upstream.
 * Dark had survived that bug on contrast alone (a near-black panel keeps
 * navy-ground text legible); what it needed was the warm ground's hues. */
:root[data-theme="dark"] .rail .brand-name,
:root[data-theme="dark"] .who .name{color:var(--ink)}

/* Shadows are pure black here, which on a near-black page means no edge at
 * all. These four relied on one, so they get a hairline — the same move the
 * colourblind block makes for .login-card. */
:root[data-theme="dark"] .rdrawer{border-left:1px solid var(--line)}
:root[data-theme="dark"] .login-card{border:1px solid var(--line)}
:root[data-theme="dark"] .skip-link{border:1px solid var(--line)}

/* Geist, self-hosted. NO CDN — this app must render with the network
 * unplugged, which is why the default theme ships no webfont at all. If
 * public/fonts/ is empty the src fails and the stack above falls through to
 * Segoe UI; the theme still works, it just is not Geist. */
@font-face{
  font-family:"Geist";
  src:url("/fonts/Geist-Regular.woff2") format("woff2");
  font-weight:400;font-style:normal;font-display:swap;
}
@font-face{
  font-family:"Geist";
  src:url("/fonts/Geist-Medium.woff2") format("woff2");
  font-weight:500;font-style:normal;font-display:swap;
}
@font-face{
  font-family:"Geist";
  src:url("/fonts/Geist-SemiBold.woff2") format("woff2");
  font-weight:600;font-style:normal;font-display:swap;
}
/* 700 is not optional padding: this file asks for 650, 700, 750 and 800 in
 * .card .v, .plat-badge, .brand-name and .ic. With 600 as the heaviest real
 * face the browser SYNTHESISES the rest, which smears badly on a near-black
 * ground. */
@font-face{
  font-family:"Geist";
  src:url("/fonts/Geist-Bold.woff2") format("woff2");
  font-weight:700;font-style:normal;font-display:swap;
}
/* The mono face. Load-bearing in exactly one place — .fl-paste and the
 * kodebarang cells, where segmented identifiers have to form columns (see
 * the --font-mono comment above). */
@font-face{
  font-family:"Geist Mono";
  src:url("/fonts/GeistMono-Regular.woff2") format("woff2");
  font-weight:400;font-style:normal;font-display:swap;
}

/* Plus Jakarta Sans — the default theme's face (design prototype, 26 Sep 2026).
 * Self-hosted, one variable file (400-800, Latin), SIL OFL — licence beside
 * it in public/fonts. Offline-safe, which is what the old no-webfont rule
 * was protecting. */
@font-face{
  font-family:"Plus Jakarta Sans";font-style:normal;font-weight:400 800;font-display:swap;
  src:url("/fonts/PlusJakartaSans-latin.woff2") format("woff2");
}
