@import url('https://fonts.googleapis.com/css2?family=Barlow:wght@400;500;600;700&family=Playfair+Display:wght@600;700&display=swap');

/* The palette for every page in this directory.
 *
 * It used to live twice: each page declared the twelve values in its own
 * <style>, and this file redeclared them underneath. Because a page's <style>
 * comes first and this stylesheet last, the values here always won — so the
 * teal-and-slate palette in the pages was dead code that nothing rendered, and
 * reading a page told you the wrong thing about what the site looked like.
 *
 * Worse: six of the seven pages carried a *dark* palette too, correctly
 * guarded, and this file overrode the light half and not the dark one while
 * declaring `color-scheme:light`. The site therefore had no dark mode, and the
 * work to give it one was already sitting in the pages, unreachable.
 *
 * So the pages no longer declare colour at all, and both themes live here.
 * Twelve values light, the same twelve redefined dark; nothing else changes
 * between them — no layout, spacing, radius, weight or size.
 *
 * WARM AND SINGLE-THEME, 10 Sep 2026. Two changes at once, both decided rather
 * than drifted into:
 *
 * The VALUES now come from the design handoff bundle by way of
 * design/src/tokens/colors.css — #17140E ink on #F9F6EE paper, a cream ground,
 * #4A5D3A positive. The app had been on a cooler set (#0B0B0C on pure white)
 * that the bundle never carried, so the built app never quite looked like the
 * design it was built from. The bundle wins.
 *
 * The DARK THEME IS GONE. The handoff has none — it is a paper design, the way
 * the four staff surfaces already were — and a dark half nobody had drawn was
 * being maintained here on guesswork. Two bugs in two days came out of exactly
 * that: a monogram that turned white on a white plate, and a locked portal that
 * painted the screen black around its sign-in sheet. The site is one theme now,
 * `color-scheme:light`, and a resident on a dark phone gets the paper app.
 *
 * Still monochrome plus two signals; still no accent colour — emphasis is
 * weight and case.
 */
:root{
  color-scheme:light;

  --brand-serif:"Playfair Display",Georgia,serif;
  --brand-sans:"Barlow",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;

  --bg:#efeadc;
  --surface:#f9f6ee;
  --tile:#dfd8c6;
  --ink:#17140e;
  --muted:#6b6455;
  --line:#cfc6b0;
  --accent:#17140e;
  --accent-ink:#f9f6ee;
  /* Text ON a signal fill. --ok and --bad are deep enough to carry paper
   * (6.66:1 and 7.12:1); the warm palette's --warn is GOLD and is not — cream
   * on it measures 2.99:1, which review caught on the substitution tags in
   * me.html at 10-13px. Gold is a light fill and takes dark text, so it gets
   * its own foreground rather than a darker gold that would stop being the
   * bundle's colour. Ink on it is 5.70:1. */
  --on-status:#f9f6ee;
  --on-warn:#17140e;
  --ok:#4a5d3a;
  --ok-soft:#e4e9dc;
  --warn:#b08a28;
  --bad:#8a3a32;
  --shadow:0 1px 0 rgba(23,20,14,.06),0 12px 30px rgba(23,20,14,.05);

  /* The heat ramp, for Scotty's match game and nothing else yet.
   *
   * Two of these three are the only colours on this site that are neither the
   * design system's nor somebody else's brand guide, and that is a decision
   * rather than a drift. The obvious move for a three-step is --ok, --warn and
   * --bad, which are already one — and it is the wrong move, because --ok and
   * --bad mean GOOD NEWS and BAD NEWS everywhere else here: a price that fell,
   * a price that rose, a line the shopper got, a line they did not.
   *
   * A cold call on /match is not bad news. It is a reader telling the desk
   * that two items are different, which is as useful an answer as the other
   * two and a good deal rarer. Dressing it in the red that marks a price going
   * up would tell them they had failed at something. So the game carries a
   * TEMPERATURE scale and the site keeps its STATUS scale, and the two do not
   * borrow from each other.
   *
   * The middle step is --warn itself: the ramp needs an amber there, the
   * palette already has exactly one, and a second would be the drift this
   * comment is arguing against. It takes ink rather than cream for the reason
   * --on-warn exists, three declarations above.
   *
   * Contrast on each fill, at the label sizes /match actually uses:
   * cold 5.54:1, warm 5.70:1, hot 4.89:1. */
  --heat-cold:#35688c;
  --heat-warm:var(--warn);
  --heat-hot:#b8481a;
  --on-heat:#f9f6ee;
  --on-heat-warm:var(--on-warn);

  /* The thirteenth value, and the only one that is not the design system's.
   * It is the plate a retailer's own logo sits on — see .rmark below. It is
   * declared here, rather than written as a bare #fff inside the rule, so that
   * the one colour on this site that answers to somebody else's brand guide is
   * visible to anyone reading the palette. */
  --mark-plate:#ffffff;

  /* And the ink that goes ON that plate. It has to be declared beside it and
   * left out of both dark blocks for the same reason: the plate does not flip,
   * so anything drawn on it must not either.
   *
   * The monogram a shop with no artwork falls back to took --ink, which DOES
   * flip — so on a device set to dark, "VE" and "C" were white letters on the
   * white plate and the shop tiles looked like they had lost their logos. This
   * is the design system's own ink, spelled out here rather than referenced,
   * because referencing --ink is exactly the bug. */
  --mark-ink:#0b0b0c;

  /* ── The series. Four hues, and the first colour here that is not a signal ──
   *
   * 12 Sep 2026, for the spend graph on /me. The note at the top of this file
   * says "still monochrome plus two signals; still no accent" and that stays
   * true of the UI: these are CHART MARKS and nothing else may take one. No
   * button, no tag, no border, no text, no panel. The moment one of them
   * paints a control, --ok and --bad stop being the only colours on this site
   * that mean something, and that is the whole of the system.
   *
   * Why the spend graph needed them. A column of four retailers is not a
   * shortfall or a surplus — nothing about Woolworths is good or bad — so the
   * first draft drew the four shops as four steps of one ink. It reads at a
   * distance and fails exactly where it has to work: a shop with R120 in a
   * R6 000 month is a four-pixel band, and at four pixels a step of grey is
   * not a shop, it is an edge.
   *
   * The values come from design/src/tokens/colors.css (--sr-series-1..4) and
   * were checked rather than chosen by eye, against this file's own --surface:
   * inside the lightness band, above the chroma floor, each over 3:1 on paper,
   * and every ADJACENT pair separated under all three kinds of colour
   * blindness. Adjacent is the scope that matters because the stacking order
   * is fixed — 1 is always the bottom of a column, 4 always the top — so 1 and
   * 3 never share an edge. Reordering them throws that away.
   *
   * They avoid the signal hues on purpose: no green near --ok, no red near
   * --bad. And identity never rests on the fill alone — every series on the
   * page carries its retailer's mark and its name beside the figure. */
  --series-1:#33538f;
  --series-2:#c67b2e;
  --series-3:#8e4c8a;
  --series-4:#9e7a24;
}


body{font-family:var(--brand-sans)}
h1,h2{font-family:var(--brand-serif);font-weight:600;letter-spacing:-.025em}

/* The masthead reads from --bg and --ink rather than fixed brand colours, so
   it inverts with the rest of the page instead of staying a light slab. */
.mast{background:var(--bg);border-bottom:1px solid var(--ink)}
/* Staff sign-in, at the foot of every page (masthead.js staffFoot). */
.staffline{max-width:1080px;margin:0 auto;padding:18px 18px 28px;display:flex;flex-wrap:wrap;gap:6px 14px;align-items:baseline;
  font-size:12px;color:var(--muted);border-top:1px solid var(--line)}
.staffline span{letter-spacing:.1em;text-transform:uppercase;font-weight:700;font-size:10.5px}
.staffline a{color:var(--muted);text-decoration:underline;text-underline-offset:3px}
.staffline a:hover{color:var(--ink)}
/* The site's version, at the foot since 5 Oct 2026 (it sat beside the brand). */
.staffline .ver{margin-left:auto;display:inline-flex;align-items:center;gap:7px;min-height:44px;
  font-variant-numeric:tabular-nums;letter-spacing:.02em}
.staffline .ver img{width:32px;height:32px;object-fit:contain;flex:none;background:var(--brand-bone,#f4f2ee);border-radius:4px}
.staffline .ver:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:4px}
/* The Telegram strip under the masthead (masthead.js tgStrip). Ink on bone,
   one line on a laptop, two on a phone; the button is the one filled thing. */
.tgstrip{background:var(--surface);border-bottom:1px solid var(--line)}
.tgstrip .in{max-width:1080px;margin:0 auto;padding:8px 18px;display:flex;align-items:center;gap:12px}
.tgstrip img{width:28px;height:28px;flex:0 0 28px}
.tgstrip p{margin:0;flex:1 1 auto;font-size:13.5px;line-height:1.35;color:var(--ink)}
.tgstrip p b{font-weight:700}
.tgstrip .tgo{flex:0 0 auto;display:inline-flex;align-items:center;min-height:36px;padding:0 14px;background:var(--ink);color:var(--bg);
  font-size:13px;font-weight:600;text-decoration:none;white-space:nowrap}
.tgstrip .tgo:hover{text-decoration:none;opacity:.85}
.tgstrip .tgx{flex:0 0 auto;width:36px;height:36px;border:0;background:none;color:var(--muted);font-size:20px;line-height:1;cursor:pointer}
@media(max-width:560px){
  .tgstrip .in{flex-wrap:wrap;gap:8px 10px;padding:10px 16px}
  .tgstrip p{flex-basis:calc(100% - 38px - 36px - 20px)}
  .tgstrip .tgo{margin-left:38px}
}
.mast .in{position:relative;max-width:1080px;margin:0 auto;padding:0 18px;min-height:74px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
.brand{display:flex;align-items:center;gap:9px;min-height:44px;color:var(--ink);text-decoration:none}
.brand:hover{text-decoration:none}
.brand-logo{width:38px;height:42px;object-fit:contain;flex:0 0 38px}
.brand-copy{display:flex;flex-direction:column;line-height:1.04}
.brand-copy b{font-size:15px;letter-spacing:-.01em}
.brand-copy small{font-size:9px;line-height:1.4;letter-spacing:.16em;text-transform:uppercase;color:var(--muted);font-weight:600}
/* The version tag beside the brand (masthead.js VERSION), linked to /versions.
   Small and muted on purpose: it is for somebody reporting a problem or curious
   what changed, not a thing every visit needs to read. */
.mast .ver{margin-left:-8px;font-size:10.5px;font-variant-numeric:tabular-nums;color:var(--muted);
  text-decoration:none;letter-spacing:.02em;padding:4px 2px}
.mast .ver:hover{color:var(--ink);text-decoration:underline}
.mast nav{display:flex;gap:20px;align-items:center;margin-left:12px;font-size:13.5px;color:var(--muted)}
.mast nav a{color:var(--muted);text-decoration:none}
.mast nav a[aria-current]{color:var(--ink);font-weight:600}
/* The unread dot on Chat. The thread lives inside an order, so without this a
   resident on any other page has no way of knowing the desk has answered. It
   is a mark, not a count: what matters is that there is something to read. */
.mast nav .dot{display:inline-block;width:7px;height:7px;margin-left:5px;
 vertical-align:2px;border-radius:50%;background:var(--bad)}
/* An author `display` beats the user agent's [hidden] rule, so the dot needs
   this or it is never hidden at all. The resident subpages each carry their own
   [hidden]{display:none !important}, which masked it — index.html does not, and
   the home page drew a permanent red mark beside Chat. */
.mast nav .dot[hidden]{display:none}
/* The item count on Basket. Unlike the dot this IS a number: what a reader on
   the shelf wants to know is not "is there a basket" but "did that add land,
   and how much is in there now". It sits on the accent so it reads as the
   basket filling, not as an alert. Hidden the same way the dot is, for the
   same reason: an author display beats [hidden]. */
.mast nav .count{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;
 margin-left:6px;padding:0 5px;border-radius:9px;background:var(--accent);color:var(--accent-ink);
 font-size:11px;font-weight:700;line-height:1;font-variant-numeric:tabular-nums;vertical-align:1px}
.mast nav .count[hidden]{display:none}
/* A change to the number is a change worth a glance: the badge swells for a
   beat when it is repainted with a new count. masthead.js adds .bump only when
   the count moved on a page already drawn, so a page load draws it still. */
.mast nav .count.bump{animation:mastcount .45s ease-out}
@keyframes mastcount{0%{transform:scale(1)}40%{transform:scale(1.35)}100%{transform:scale(1)}}
@media(prefers-reduced-motion:reduce){.mast nav .count.bump{animation:none}}
/* ── Scotty: a nav item that discloses ──────────────────────────────────────
 *
 * The seventh entry in the row is a GROUP, not a destination — /search, /play
 * and /movers, the three screens that are open to anyone. masthead.js draws it
 * with the same .acctmenu the staff pill and the account control use, so the
 * masthead has one disclosure pattern rather than two.
 *
 * One difference, and it is deliberate: this menu anchors to its control's LEFT
 * edge. The other two sit at the END of the row and open leftwards into the
 * page; this one sits in the middle of the nav, and a right-anchored box would
 * open back across the links it came from. `.acctmenu.left` is that, and
 * nothing else. */
.navdrop{position:relative;display:inline-flex}
.navdrop > button{display:inline-flex;align-items:center;gap:6px;min-height:44px;padding:0;
 border:0;background:none;cursor:pointer;font:inherit;font-size:13.5px;color:var(--muted)}
.navdrop > button:hover{color:var(--ink)}
.navdrop.on > button,
.navdrop > button[aria-current]{color:var(--ink);font-weight:600}
.acctmenu.left{left:0;right:auto}
/* Two lines per row, because the labels are short and the difference between
   them is not. The name takes .acctmenu's own size; the note is the .em size,
   which is the only other type this menu already has. */
.acctmenu li a b{display:block;font-size:13.5px;font-weight:600}
.acctmenu li a .note{display:block;font-size:12px;line-height:1.35;color:var(--muted);
 /* Stated rather than inherited: on the row for the page you are ON, the
    `.mast nav a[aria-current]` rule above weights the whole LINK at 600, and
    the note took that with it — the one note that should read quietest read
    heaviest. The colour never drifted because it is named here; the weight is
    named here now for the same reason. */
 font-weight:400}
.acctmenu li a[aria-current]{background:var(--tile)}

.mast .right{margin-left:auto;display:flex;gap:10px;align-items:center}
/* The basket, as the Mini App draws it (app.css .basketgo): an icon beside the
   account control, with the count on its shoulder. Not in the nav row, and not
   drawn signed out — masthead.js toggles [hidden] with the session. */
.mast .basketgo,.mast .chatgo,.mast .callgo{position:relative;display:flex;align-items:center;justify-content:center;
 width:44px;height:44px;color:var(--muted);text-decoration:none;border-radius:999px}
.mast .basketgo:hover,.mast .basketgo[aria-current],.mast .chatgo:hover,.mast .chatgo[aria-current],.mast .callgo:hover{color:var(--ink)}
/* Chat's unread dot, on the icon's shoulder (5 Oct 2026: Chat left the row for
   an icon beside the basket, as the Mini App draws it). */
.mast .chatgo .dot{position:absolute;top:9px;right:9px;width:8px;height:8px;border-radius:50%;background:var(--bad)}
.mast .chatgo .dot[hidden]{display:none}
.mast .basketgo[hidden]{display:none}
.mast .basketgo .count{position:absolute;top:3px;right:3px;min-width:16px;height:16px;padding:0 4px;
 border-radius:999px;background:var(--ink);color:var(--surface);font-size:10px;line-height:16px;
 font-weight:600;text-align:center;font-variant-numeric:tabular-nums;font-style:normal}
.mast .basketgo .count[hidden]{display:none}
.mast .basketgo .count.bump{animation:mastcount .45s ease-out}
@media(prefers-reduced-motion:reduce){.mast .basketgo .count.bump{animation:none}}
.pill{border:1px solid var(--line);border-radius:999px;padding:5px 12px;font-size:11.5px;color:var(--muted);white-space:nowrap}

/* ── The account control in the masthead ────────────────────────────────────
 *
 * masthead.js draws it on every page. It replaces auth-pill.js, which mounted a
 * second bar above the page heading — so five pages carried two account
 * controls that could disagree with each other, and the other five carried none.
 *
 * The styles live here rather than in the module for the same reason the
 * palette does: a colour or a corner written in script runs after this
 * stylesheet and nothing can correct it. */
.acct{position:relative;flex:0 0 auto}
.acct>button{display:flex;gap:8px;align-items:center;cursor:pointer;font:inherit;font-size:13px;
 border:1px solid var(--line);background:var(--surface);color:var(--ink);
 border-radius:999px;padding:5px 13px 5px 6px;max-width:230px;min-height:44px}
.acct>button:hover{border-color:var(--ink)}
.acct .av{width:24px;height:24px;flex:0 0 24px;border-radius:50%;background:var(--accent);
 color:var(--accent-ink);display:flex;align-items:center;justify-content:center;
 font-size:11.5px;font-weight:700;text-transform:uppercase}
.acct .lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.acct .caret{opacity:.5;font-size:10px}

/* Signing in is the masthead's one call to action, so it is the one FILLED
 * control on the bar — the only thing up there that is not an outline or a
 * label. It takes the account control's pill geometry, because it is the
 * control the account pill replaces the moment it is used, and the bar should
 * not change shape underneath a resident who has just signed in.
 *
 * It carried `.btn` and brand.css has no .btn rule, so the page decided: an
 * outlined box on the two pages that declare a .btn of their own, an underlined
 * link on the seven that do not. The most important control on the masthead was
 * the one whose look was an accident of which page you were standing on. */
.acct .signin{display:inline-flex;align-items:center;justify-content:center;
 min-height:44px;padding:5px 18px;border:1px solid var(--accent);border-radius:999px;
 background:var(--accent);color:var(--accent-ink);
 font:inherit;font-size:13px;font-weight:600;text-decoration:none;white-space:nowrap}
/* --accent IS --ink on this palette, so a hover cannot darken the fill and a
   border change would be invisible against it. It lifts instead. */
.acct .signin:hover{opacity:.86;text-decoration:none}
.acctmenu{position:absolute;right:0;top:calc(100% + 7px);z-index:40;margin:0;padding:5px;
 min-width:200px;list-style:none;background:var(--surface);border:1px solid var(--ink);display:none}
.acctmenu.open{display:block}
.acctmenu .em{display:block;padding:7px 10px 8px;font-size:12px;color:var(--muted);
 border-bottom:1px solid var(--line);margin-bottom:4px;overflow-wrap:anywhere}
.acctmenu a,.acctmenu button{display:block;width:100%;text-align:left;font:inherit;font-size:13.5px;
 padding:9px 10px;border:0;background:none;color:var(--ink);text-decoration:none;cursor:pointer}
.acctmenu a:hover,.acctmenu button:hover{background:var(--tile);text-decoration:none}
.acctmenu button.out{color:var(--bad)}
/* A door that is not a resident's says so rather than being removed — see the
 * note about /review in masthead.js. */
.acctmenu .tag{font-size:10.5px;letter-spacing:.07em;text-transform:uppercase;
 color:var(--muted);font-weight:700;margin-left:6px;vertical-align:1px}

/* The staff door. Same mechanics as the account control, quieter: it is a way
 * in for four people, sitting beside a control the whole estate uses, so it
 * reads as chrome until somebody is looking for it. Its label is `.slbl` rather
 * than `.lbl` on purpose — the account control drops its label on a phone and
 * keeps its avatar, and this one has no avatar to fall back on.
 *
 * It was a pill, and "quieter" was only a muted label inside the same outline
 * the account control wears. Four people's door drawn as the equal of the whole
 * estate's — and, next to a Sign in that no stylesheet was drawing at all, as
 * the louder of the two. So it loses the outline and keeps the 44px target: a
 * muted label and a caret, underlined on hover, which is what every other piece
 * of secondary text on this bar does. Nothing about who may open it changes —
 * the pill was navigation, not a permission, and so is this. */
.staffpill>button{padding:5px 4px;gap:5px;color:var(--muted);max-width:none;
 background:none;border-color:transparent;border-radius:0}
.staffpill>button:hover{color:var(--ink);border-color:transparent;text-decoration:underline}
.staffpill .slbl{white-space:nowrap;letter-spacing:.02em}
.staffpill .caret{opacity:.42}

/* The small-screen disclosure. Hidden wherever the nav itself is shown. */
.mastmenu{display:none;font:inherit;font-size:12px;letter-spacing:.06em;text-transform:uppercase;
 font-weight:600;cursor:pointer;background:var(--surface);color:var(--ink);
 border:1px solid var(--line);border-radius:2px;padding:0 13px;min-height:44px;margin-left:auto}

.brand-hero{display:grid;grid-template-columns:minmax(0,1fr) 310px;align-items:center;gap:28px;margin:-14px 0 18px}
.brand-hero .lede{margin-bottom:0}
.eyebrow{margin:0 0 10px;color:var(--accent);font-size:11px;letter-spacing:.16em;text-transform:uppercase;font-weight:700}
/* The mascot sits on a bone plate that does not flip with the theme, for the
   same reason a retailer's mark sits on --mark-plate: his jacket is near-black,
   and on the dark ground it reads as a pair of eyes and a trolley. The plate is
   the catalogue's rule (docs/design/scotty-reference, "Using him"); the game's
   .snappy panel already does the same with --brand-bone. */
.mascot-stage{align-self:end;display:flex;justify-content:center;align-items:flex-end;min-height:270px;position:relative;overflow:hidden;border-bottom:1px solid var(--ink);background:var(--brand-bone,#f4f2ee)}
.mascot-hero{display:block;width:min(100%,290px);max-height:300px;object-fit:contain;object-position:center bottom;filter:drop-shadow(0 16px 18px rgba(11,11,12,.12))}

/* Retailer marks.
 *
 * The one place in this stylesheet a colour outside the palette is allowed, and
 * it is quarantined rather than admitted: a retailer's logo is drawn in its own
 * brand colours, so it sits on a fixed white plate and the palette outside that
 * plate is untouched. Nothing here paints UI with a retailer's colour.
 *
 * --mark-plate is white in both themes, and that is the point — these marks are
 * full-colour artwork drawn for a light ground. A --surface plate would go dark
 * with the theme and either mangle a dark mark or force per-retailer inversions
 * that nobody will maintain. sync-app-palette.mjs checks the twelve system
 * tokens and does not police stray literals, so this is named in :root instead
 * of written inline — an exception the palette declares rather than hides.
 *
 * Height-constrained, width-flexible: one of these brands (Woolworths) has a
 * symbol that survives shrinking, and the others (Checkers, Pick n Pay, SPAR)
 * are wordmarks that would be a smudge in a square. A symbol sits square in the
 * plate, a wordmark runs long, and both land at the same height.
 *
 * The sibling rule is the whole fallback mechanism: a wordmark already says the
 * retailer's name, so the text label beside it is hidden while a mark renders.
 * A missing asset removes its own <img> (retailers.js), which stops matching
 * this selector, which brings the text back — no JS, no state, no flash of an
 * empty plate, because the plate is the image rather than a box around it. */
.rmark{
  display:block;height:19px;width:auto;max-width:84px;flex:0 0 auto;
  object-fit:contain;object-position:left center;
  background:var(--mark-plate);border:1px solid var(--line);padding:2px 4px;box-sizing:border-box;
}
.rmark + .rname,
.rmark + .rlabel{display:none}
/* .rname carries a page's own uppercase column styling; .rlabel is the same
 * mechanism where the text is just chip copy and should stay chip copy. */
.cell .rmark{margin-bottom:5px}
.stophead .rmark,
.chip .rmark,
.shopchip .rmark{align-self:center;margin-right:2px}
/* A retailer that does not stock the item is context, not a brand placement. */
.cell.none .rmark{opacity:.45;filter:grayscale(1)}

/* Radius. The system rounds nothing except pills and dots — panels take a 4px
 * hairline corner, controls and tags 2px.
 *
 * The first pass at this named .card, .btn, .field and .chip and stopped, which
 * left .cell at 11px, .thumb at 7px and .ico at 9px still rounded on the
 * busiest page on the site. A blanket reset cannot do the job — `*` loses to
 * every class rule in the pages — so the list has to be complete, and this is
 * the whole of it. To regenerate the inventory after adding a rule:
 *
 *   node -e 'const fs=require("fs");for(const f of process.argv.slice(1)){
 *     const css=(fs.readFileSync(f,"utf8").match(/<style>[\s\S]*?<\/style>/g)||[]).join("\n");
 *     const re=/([^{}]+)\{([^}]*border-radius:\s*([0-9a-z%.]+)[^}]*)\}/g;let m;
 *     while((m=re.exec(css)))console.log(f,m[3],m[1].replace(/\s+/g," ").trim().slice(-60));
 *   }' compare/*.html
 *
 * Anything it prints that is not 999px or 50% belongs below. */
.card,
.callout,
.recovery,
.tablewrap,
.side,
.cand,
.privacy,
.acctmenu,
.page img{border-radius:4px}

.card{box-shadow:none}

.btn,
.field,
textarea,
.primary,
.chip,
.cell,
.thumb,
.ico,
.tag,
.tier,
.sub,
.recheck,
:focus-visible,
.head code,
.savebox input,
.paid input,
.empty button,
.idx .key i,
#gate input,
#keybar input,
#keybar button,
/* The sign-in link beside the review key. It is a link styled as the primary
   button next to it, so it was written with the button's old 9px and missed
   when the list was compiled — the exact maintenance trap the note above
   describes. Added here and on main at the same time, independently, which is
   its own argument for the note: sync-app-palette.mjs had been reporting it,
   and because the compare site's build command runs that check, the site could
   not deploy at all while it stood. */
#keybar .signin,
.si input,
.si textarea,
.si-choice .btn,
.rmark{border-radius:2px}

/* Item ratings.
 *
 * Two surfaces draw these — the shop shelf and /ratings — and compare/*.js may
 * not inject a radius (sync-app-palette.mjs holds it to system values, because
 * anything ratings.js writes into the head runs after this file and nothing
 * could square it). So the markup lives in ratings.js and the look lives here,
 * which is the one stylesheet both surfaces already load.
 *
 * There is no accent colour in this system, so a star cannot be gold. Filled is
 * ink, empty is ink at 16%, and a partial star on a 4.6 average is a filled
 * layer clipped over an empty one by percentage width — which is what .stars,
 * .stars-fill and the two .stars-row copies are for. Both rows must be the same
 * five glyphs at the same size or the clip lands in the wrong place.
 *
 * The stars take `currentColor`, so a rating strip inside an inverted panel
 * inherits rather than needing a second palette. */
.stars{position:relative;display:inline-block;line-height:0;color:var(--ink)}
/* width:max-content and flex:none are load-bearing, not tidiness. The filled
   layer sits in a box narrowed to the average — 46% for 2.3 — and a flex row
   inside a narrow box SHRINKS its children by default. So the five filled
   stars were being squeezed to fit rather than clipped, and every average
   rendered as a jumble of two star sizes on top of each other. The row has to
   keep its natural width and let overflow do the clipping. */
.stars-row{display:flex;gap:2px;width:max-content}
.stars-row .star{display:block;flex:none}
.stars-fill{position:absolute;left:0;top:0;overflow:hidden;line-height:0}

/* The average. Never a button: it reads, and the count is always beside it so
 * a figure is never mistaken for a verdict on its own. */
.shelf{display:inline-flex;align-items:center;gap:8px;min-height:20px}
.shelf-thin .stars{opacity:.5}
.shelf-figure{font-size:15px;font-weight:700;letter-spacing:-.01em;font-variant-numeric:tabular-nums}
.shelf-count{font-size:12.5px;color:var(--muted);font-variant-numeric:tabular-nums}
.shelf-note,.shelf-none{font-size:12.5px;color:var(--muted)}

/* The control. The only pressable thing in a rating strip, and sized so it is
 * obviously that: 38px here, 44px on a phone and wherever .rate-large is used. */
.rate{display:inline-flex;gap:4px;align-items:center}
.rate-star{display:inline-flex;align-items:center;justify-content:center;
 width:38px;height:38px;padding:0;border:1px solid var(--line);border-radius:2px;
 background:var(--surface);color:var(--ink);cursor:pointer}
.rate-star.on{background:var(--tile)}
.rate-star[disabled]{opacity:.55;cursor:not-allowed}
.rate-large .rate-star{width:44px;height:44px}
.rate-lab{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:700}
.rate-note{font-size:12.5px;color:var(--muted)}

@media(max-width:560px){
  .rate-star{width:44px;height:44px}
  .rate{gap:6px}
}

@media(max-width:760px){
  .brand-hero{grid-template-columns:1fr;margin-top:0;gap:8px}
  .mascot-stage{min-height:180px;justify-content:flex-end}
  .mascot-hero{width:210px;max-height:210px}
}
@media(max-width:560px){
  .rmark{height:17px;max-width:72px}
  .brand-logo{width:32px;height:36px;flex-basis:32px}
  .brand-copy b{font-size:14px}
  .brand-copy small{font-size:8px}
  /* The nav used to be hidden here full stop, with nothing in its place — the
   * one viewport where a visitor most needs navigation had none. It is now
   * behind the Menu button, which only exists at this width. */
  .mast .pill{display:none}
  .mastmenu{display:inline-flex;align-items:center}
  .mast .in{flex-wrap:wrap;padding-top:9px;padding-bottom:9px}
  .mast .right{margin-left:0;order:3}
  .mast nav{display:none;order:4;flex-basis:100%;flex-wrap:wrap;gap:0 18px;margin-left:0}
  .mast[data-open] nav{display:flex}
  .mast nav a{padding:9px 0;min-height:44px;display:flex;align-items:center}
  /* A nested drop-down inside a disclosure is one tap too many on a phone, and
     there is nowhere for it to hang: the nav is a wrapping row the full width
     of the screen. So the group stops being a menu and becomes what it is —
     four links under a heading — and the button stops being a button. The
     four still carry their 44px rows through the rule above.

     They are drawn as a wrapping ROW, like the six links above them, and not
     as a stack of menu rows. Stacked, with a note under each label, the group
     stood taller than the rest of the menu put together and read as a second
     section of the site rather than four more pages of this one — which is
     the opposite of what the nav entry is for. */
  .mast .navdrop{position:static;flex-basis:100%;flex-wrap:wrap}
  /* A label, not a control: nothing taps it, so it does not hold the 44px a
     tappable row does — that height on a line nobody presses is what pushed
     the group's own links a thumb's width down the menu. */
  .mast .navdrop > button{flex-basis:100%;justify-content:flex-start;
    border-top:1px solid var(--line);margin-top:4px;min-height:0;padding:10px 0 2px;
    font-size:11px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;
    color:var(--muted);cursor:default}
  .mast .navdrop > button .caret{display:none}
  .mast .navdrop .acctmenu{display:flex;flex-wrap:wrap;gap:0 18px;position:static;
    flex-basis:100%;border:0;background:none;padding:0;min-width:0;max-width:none}
  .mast .navdrop .acctmenu .em{display:none}
  /* Inside the phone menu these are nav LINKS, not menu rows, and they are
     drawn as the links above them are: one line, the nav's own size and colour,
     flush with them under the Scotty heading (an indent read as a mistake). Carrying the drop-down's two-line rows
     down here is what made the group read as a section of its own rather than
     part of the nav — four two-line entries with a note apiece stood taller
     than the whole of the rest of the menu, and the notes repeated what the
     page says in its own lede a tap later. The 44px row stays; only the second
     line goes — the note is the drop-down's job on a screen wide enough to
     hang one, and docs/design/scotty-nav/Phone.dc.html draws none either. */
  .mast .navdrop .acctmenu li a{display:flex;align-items:center;min-height:44px;
    padding:9px 0;font-size:13.5px;color:var(--muted);background:none;white-space:nowrap}
  .mast .navdrop .acctmenu li a b{font-size:13.5px;font-weight:400}
  .mast .navdrop .acctmenu li a .note{display:none}
  /* The page you are on is marked the way the nav marks it — weight and ink,
     not a filled band. .acctmenu's tile is for a boxed menu; here there is no
     box, so a full-width band across a wrapping nav row reads as a heading. */
  .mast .navdrop .acctmenu li a[aria-current]{color:var(--ink)}
  .mast .navdrop .acctmenu li a[aria-current] b{font-weight:600}
  .acct .lbl{display:none}
  .acct>button{padding:5px 6px}
  /* Below this width the header wraps and .right loses its margin-left:auto,
   * so the controls can sit at the LEFT edge instead of the right. A drop-down
   * pinned with right:0 against its own control then opens leftwards off the
   * screen: at 320px the staff menu's box started at x=-127, and all a person
   * saw was the 73px of empty padding that fitted — which reads as an empty
   * menu rather than a broken one.
   *
   * Anchoring to the control's LEFT edge instead is not the fix. WHETHER the
   * header wraps depends on the page's own content, not on the width: at 390px
   * /shop wraps and / does not, so left-anchoring pushed the menu off the RIGHT
   * edge on the pages that had not wrapped. A width breakpoint cannot know.
   *
   * So on a phone the menus stop hanging off their control at all. .acct goes
   * position:static, which hands the containing block to .mast .in — the
   * header's own row box, which is always exactly as wide as the screen less
   * its padding. The menu opens under the whole header, inside the screen, at
   * every width and whether the header wrapped or not. */
  .mast .acct{position:static}
  .mast .acctmenu{left:auto;right:18px;min-width:0;width:max-content;
    max-width:calc(100vw - 36px)}
}

/* ── The price movers band ──────────────────────────────────────────────────
 *
 * Shared because the band is shared: `/compare` draws the full board and `/`
 * the top three of the same rows, from one module (movers.js). The styles live
 * here rather than in either page for the same reason the palette does — two
 * copies of a component is how the three retailer name tables happened.
 *
 * THE ONE RULE THIS BLOCK EXISTS TO HOLD
 *
 * Two different things move on this board and they must never share a colour.
 * A PRICE moving is money, so it takes --ok and --bad — down is good here,
 * which is the opposite way round to a stock ticker. A SLOT moving is only the
 * board rearranging itself, so it is drawn in --accent: a left rail and the old
 * rank in words. A row that climbed from #5 to #3 has not saved anybody a cent,
 * and painting that green would say it had.
 *
 * Anything painting --ok, --warn or --accent as a FILL takes its foreground
 * token with it (--on-status, --accent-ink), per the note at the top of this
 * file: white on the lifted dark --warn is 2.92:1 and fails.
 */
.mvband{display:block;margin:34px 0 0}
.mvband[hidden]{display:none}

.mvhead{border-top:1px solid var(--ink);padding-top:22px;margin-bottom:14px;
  display:flex;align-items:flex-end;gap:20px;flex-wrap:wrap}
.mvhead > div:first-child{flex:1 1 320px;min-width:0}
.mvkick{margin:0 0 9px;font-size:11px;font-weight:700;letter-spacing:.16em;
  text-transform:uppercase;color:var(--muted)}
.mvhead h2{margin:0 0 6px;font-size:24px;line-height:1.18}
.mvhead.teaser h2{font-size:22px;line-height:1.2}
.mvlede{margin:0;color:var(--muted);font-size:13.5px;line-height:1.55;max-width:72ch}

/* A pass that aborted its delisting below the coverage floor. The board has to
 * inherit mark_unseen_variants' honesty rather than quietly ranking two
 * retailers as though it had seen three. */
.mvwarn{margin:0 0 12px;padding:10px 13px;border:1px solid var(--warn);border-radius:4px;
  font-size:13px;color:var(--ink)}

.mvtabs{flex:0 0 auto;display:flex;border:1px solid var(--ink);border-radius:2px;overflow:hidden}
.mvtabs.pill{border-color:var(--line);border-radius:999px}
.mvtabs .mvchip{border:0;border-radius:0;height:40px;padding:0 18px;font-weight:600}
.mvtabs.pill .mvchip{height:36px;padding:0 16px;font-weight:400}
.mvtabs .mvchip + .mvchip{border-left:1px solid var(--ink)}
.mvtabs.pill .mvchip + .mvchip{border-left-color:var(--line)}

.mvchip{display:inline-flex;align-items:center;height:36px;padding:0 15px;cursor:pointer;
  font:inherit;font-size:13px;background:var(--surface);color:var(--ink);
  border:1px solid var(--line);border-radius:999px}
.mvchip.on{background:var(--accent);border-color:var(--accent);color:var(--accent-ink)}

.mvcard{padding:20px}
.mvctl{display:flex;gap:9px;align-items:center;flex-wrap:wrap;
  padding-bottom:15px;border-bottom:1px solid var(--line)}
.mvhd{font-size:10.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--muted)}
.mvsep{width:1px;height:22px;background:var(--line);margin:0 5px}
.mvcount{margin-left:auto;font-size:12.5px;color:var(--muted)}

/* The "now" column carries a promo tag beside its price, and 116px fitted the
 * price alone — the tag spilled into the move column next to it. */
.mvrow{display:grid;grid-template-columns:36px minmax(0,1fr) 128px 132px 176px;gap:14px;
  align-items:center;padding:13px 0 13px 10px;margin-left:-12px;
  border-bottom:1px solid var(--tile);border-left:2px solid transparent}
.mvtable.savings .mvrow{grid-template-columns:36px minmax(0,1fr) 150px 150px 132px}
.mvrow.head{align-items:end;padding:14px 0 9px 10px;border-bottom:1px solid var(--line)}
.mvrow.head > div{font-size:10.5px;font-weight:700;letter-spacing:.09em;
  text-transform:uppercase;color:var(--muted)}

/* The board's own churn, and the only thing here allowed to be loud. */
.mvrow.changed{border-left-color:var(--accent);
  background:color-mix(in srgb,var(--tile) 45%,var(--surface))}

.mvrank b{font-family:var(--brand-serif);font-weight:600;font-size:17px;line-height:1;display:block}
.mvchurn{display:block;margin-top:4px;font-size:10.5px;font-weight:700;letter-spacing:.06em;
  text-transform:uppercase;color:var(--muted);white-space:nowrap}

.mvitem{display:flex;gap:11px;align-items:center;min-width:0}
.mvitem > span:last-child{min-width:0}
.mvitem b{display:block;font-weight:600;font-size:14.5px;line-height:1.3;overflow-wrap:anywhere}
.mvthumb{width:44px;height:44px;flex:0 0 44px;background:var(--tile);border:1px solid var(--line);
  border-radius:2px;overflow:hidden;display:flex;align-items:center;justify-content:center}
.mvthumb.empty{background:var(--bg);border-style:dashed}
.mvthumb img{width:100%;height:100%;object-fit:contain;display:block}
.mvsub{display:block;font-size:12.5px;color:var(--muted)}

/* The price and whatever sits beside it — a promo tag, and on a phone the word
 * naming the cell — on one wrapping line, so a long pair drops rather than
 * pushes into the column next door. The retailer's mark keeps its own line. */
.mvbest{display:flex;flex-wrap:wrap;align-items:center;column-gap:7px;row-gap:3px}
.mvat .mvr,.mvbest .mvr{display:flex;align-items:center;gap:5px;margin-bottom:3px}
.mvbest .mvr{flex-basis:100%;margin-bottom:0}
.mvr .rlabel{font-size:10.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--ink)}
.mvr.ok .rlabel{color:var(--ok)}
/* Both money columns, not just the first. .mvbest held a price and never said
 * so, so the "now" price came out sans-serif and a size down from the "was"
 * beside it — the two numbers the board exists to put next to each other, set
 * as though they were different kinds of thing. */
.mvat b,.mvbest b{font-family:var(--brand-serif);font-weight:600;font-size:17px}
.mvmove{text-align:right}
.mvmove b{display:block;font-family:var(--brand-serif);font-weight:600;font-size:19px;line-height:1.1}
.mvmove .mvsub{margin-top:2px}
.money.ok{color:var(--ok)}
.money.bad{color:var(--bad)}

.mvtag{display:inline-flex;align-items:center;height:19px;padding:0 7px;border-radius:2px;
  font-size:9.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;vertical-align:1px}
.mvtag.warn{background:var(--warn);color:var(--on-warn)}
/* Filled for the board's own churn on a teaser tile, outlined for "moved
 * again last night" — that is a secondary state on the weekly board and a
 * filled tag makes it compete with the rank rail beside it. */
.mvtag.ink{background:var(--accent);color:var(--accent-ink)}
.mvtag.outline{border:1px solid var(--ink);color:var(--ink)}
.mvtag.sub{border:1px solid var(--warn);color:var(--warn)}

/* A row that simply vanishes reads as a bug; a row that says goodbye reads as
 * a board. Its own grid, because it has nothing to say in the price columns. */
.mvrow.gone{grid-template-columns:36px minmax(0,1fr) 176px;border-bottom:0;
  border-top:1px dashed var(--line);margin-top:2px;opacity:.62}
.mvgone{font-size:10.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--muted)}

/* The quiet night, and the empty "only what changed" filter. Deliberately
 * plain: a board that finds drama every morning is one people stop reading, so
 * finding nothing has to be a legible answer rather than a broken-looking gap. */
.mvempty{margin:0;padding:34px 0;text-align:center;font-size:13.5px;line-height:1.6;
  color:var(--muted);border-bottom:1px solid var(--line)}

.mvmethod{margin:18px 0 0;padding-top:14px;border-top:1px solid var(--line);
  font-size:12.5px;line-height:1.6;color:var(--muted)}

/* The teaser on `/`: the top three of the same rows. */
.mvtiles{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-bottom:14px}
.mvtile{padding:15px 16px;border-left:2px solid var(--line)}
.mvtile.changed{border-left-color:var(--accent)}
.mvtiletop{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:9px;min-height:19px}
.mvtile > b{display:block;font-weight:600;font-size:15px;line-height:1.3}
.mvbig{display:flex;align-items:baseline;gap:9px;margin-top:10px}
.mvbig b{font-family:var(--brand-serif);font-weight:600;font-size:26px;letter-spacing:-.02em}
.mvbig span{font-size:13px;color:var(--muted)}
.mvtilefoot{margin-top:9px;padding-top:9px;border-top:1px solid var(--tile)}
.mvfoot{display:flex;gap:14px;align-items:center;flex-wrap:wrap}
.mvfoot .mvsub{max-width:52ch}

@media(max-width:900px){
  .mvtiles{grid-template-columns:1fr}
  /* The five-column grid stops being a table and becomes TWO lines, not five.
   * A horizontal scroller would be worse here than on the comparison table:
   * that one has a fixed column per retailer, this one does not.
   *
   * The first cut stacked every cell in the one column, which put the two
   * prices and the move on three lines of their own under each name and ran a
   * phone row to 190–240px. Worse than the height: with the head row hidden,
   * the two prices sat one above the other with nothing saying which was which,
   * and the whole board is the difference between them.
   *
   * They are three SHORT things, so they keep their row instead: the name takes
   * the top line, the money takes the line under it, and the move stays
   * right-aligned at the end of that line where the eye already looks for it.
   * 153px on the same rows, and each price named by the page that draws it.
   *
   * Addressed by position rather than by class, because the cells are not in
   * the same order on every board — the savings board puts its move in the
   * middle — and "third, fourth, fifth cell" is true of all of them. */
  .mvrow,.mvtable.savings .mvrow{
    grid-template-columns:48px auto minmax(0,1fr) auto;
    gap:9px 12px}
  /* 36px holds "held" and "new" and nothing else; "was 10th" does not wrap, so
   * on the narrow board it ran under the thumbnail beside it. */
  .mvchurn{font-size:10px}
  .mvrow > div:nth-child(1){grid-column:1;grid-row:1;align-self:start}
  .mvrow > div:nth-child(2){grid-column:2 / -1;grid-row:1}
  .mvrow > div:nth-child(3){grid-column:2;grid-row:2}
  .mvrow > div:nth-child(4){grid-column:3;grid-row:2}
  .mvrow > div:nth-child(5){grid-column:4;grid-row:2;justify-self:end}
  .mvrow.head{display:none}
  .mvrow.gone{grid-template-columns:48px minmax(0,1fr)}
  .mvat,.mvbest{min-width:0}
  /* The move is the shortest of the three and the one nobody should have to
   * reassemble across two lines, so it keeps its width and the cell beside it
   * — which can wrap a promo tag under its price — gives up the room. */
  .mvmove{text-align:right;align-self:end}
  .mvmove .mvsub{white-space:nowrap}
  .mvat b,.mvbest b{font-size:16px}
  .mvmove b{font-size:17px}
  /* The label, its chips, then the next label: a phone row fits one group, and
   * a wrap that lands mid-group reads as a single strip of six. The rule is
   * the separator once the groups are on their own lines. */
  .mvctl{gap:8px}
  .mvctl .mvhd{flex-basis:100%;margin-top:4px}
  .mvctl .mvhd:first-child{margin-top:0}
  .mvsep{display:none}
  .mvcount{margin-left:0;width:100%}
  .mvchip{min-height:44px}
  .mvtabs .mvchip{height:44px}
}

/* The oldest phones still in the estate. Three money cells and a rank fit 360
 * even at the worst the board can produce — a four-figure pair, a promo tag and
 * a wordmark in the same row — and do not fit 320. So the move drops to a line
 * of its own there rather than being run over by the tag above it. */
@media(max-width:344px){
  .mvrow,.mvtable.savings .mvrow{grid-template-columns:44px auto minmax(0,1fr)}
  .mvrow > div:nth-child(5){grid-column:2 / -1;grid-row:3;justify-self:end}
  .mvrow.gone{grid-template-columns:44px minmax(0,1fr)}
}

/* Scotty on a state — the head icon, not a pose.
 *
 * docs/brand/scotty/ASSETS.md on the fifteen files in app/assets/scotty:
 * thirteen are 139px crops of the May thumbnail, they "hold the filenames and
 * nothing else", and the instruction is "Do not put one on a surface." Only
 * scotty-shopping.webp and scotty-icon.webp are real artwork.
 *
 * Four of those crops were on surfaces — searching, empty and winner added with
 * the states, concierge and oops already on the sign-in sheet — and all five
 * states draw scotty-icon.webp now: a 256px head crop of the September master.
 *
 * The cost is real and is the whole of it: a state no longer has its own pose.
 * `searching`, `empty`, `oops` and `winner` each said something the head does
 * not. That difference was mostly notional, since at any usable size a 139px
 * thumbnail is a dithered smudge carrying a sliver of the neighbouring tile,
 * but it was not nothing. The poses come back, one per state, when they are
 * drawn — SCOTTY-SPEC.md's rule is to cite the pose by name, and the five names
 * are in scripts/scotty.test.mjs waiting for files that deserve them.
 *
 * SCOTTY-SPEC.md, "UI usage", is why the icon is the right stand-in rather than
 * a shrunken figure: it is the answer for anything under 120px, its own floor
 * is 24px, and it is the ONE exception to the bone-plate rule. It is mostly fur
 * at #8C8C8C and holds on the dark card unaided, which is the argument for
 * keeping the icon a head rather than a small body — so there is no plate here,
 * and none is needed.
 */
.scotty{display:block;width:72px;height:72px;object-fit:contain;margin:0 auto 12px}
/* Beside the figure rather than above it: the savings hero reads as one object
   with the number leading. */
.scotty-beside{display:flex;gap:14px;align-items:center}
.scotty-beside .scotty{margin:0;flex:0 0 auto}

/* The staff seat bar — app/staff/seat.js, one line at the top of a staff page.
 *
 * It has to sit on four surfaces that carry their own palettes in their own
 * <style> blocks (the DC prototype runtime keeps each page self-contained), and
 * on this stylesheet's palette when anything else mounts it. So it names four
 * roles and maps each to whichever of the two declares it, rather than writing
 * a literal: every colour below resolves to a token the surrounding page has
 * declared, which is what scripts/audit-surfaces.mjs checks on every element it
 * samples. A bare #625f59 here fails that check on all four staff pages, and it
 * deserves to — it would be a sixth grey nobody chose. */
.seatbar,#seatGate,.seat-inbox{
 --seat-ground:var(--paper,var(--surface));
 --seat-ink:var(--ink);
 --seat-line:var(--desk,var(--line));
 --seat-warn:var(--neg,var(--bad));
 /* One step back from the sheet's own paper, for the things a reader is meant
    to fill in or tap — see the note at #seatGate below. Named against --bone
    rather than --bg because --bg is what this maps ONTO, and a custom property
    that mentions the one deriving it is a cycle: both go invalid, on every
    staff surface at once. --tile is the resident palette's version of the same
    step, for anything that mounts the sheet on this stylesheet's own colours. */
 --seat-field:var(--bone,var(--tile));
}
.seatbar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;
 padding:8px 16px;background:var(--seat-ground);border-bottom:1px solid var(--seat-line);
 color:var(--seat-ink);font:13px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,Helvetica,Arial,sans-serif}
/* Quiet, without naming a sixth grey: two of the four staff palettes declare a
   muted token and two do not, and the one this stylesheet declares is not in
   the design system's list. Ink at 62% is ink, on every palette. */
.seat-who{color:var(--seat-ink);opacity:.62}
.seat-btn{border:1px solid var(--seat-ink);background:var(--seat-ink);color:var(--seat-ground);
 border-radius:2px;padding:7px 14px;font:inherit;font-weight:600;cursor:pointer;min-height:36px}
.seat-btn[disabled]{opacity:.5;cursor:default}
.seat-ghost{background:transparent;color:var(--seat-ink);border-color:var(--seat-line);margin-left:auto}
/* The way back to the site, first on the bar. It must NOT take the auto margin
   .seat-ghost carries — that margin is what holds Sign out at the right-hand
   end, and the first ghost element on the row is the one that claims it. */
.seat-home{margin-left:0;margin-right:2px;display:inline-flex;align-items:center;
 text-decoration:none}
.seat-home:hover{border-color:var(--seat-ink)}
/* The portal switcher — the other three portals, on the bar of the one you are
 * in. It sits between the identity and Sign out, so it takes the auto margin
 * and Sign out keeps the right-hand end. Its own buttons must not each claim
 * one, or they would spread across the bar. */
.seat-switch{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-left:auto}
.seat-switch .seat-ghost{margin-left:0;text-decoration:none;display:inline-flex;
 align-items:center;font-weight:500}
.seat-switch .seat-ghost:hover{border-color:var(--seat-ink)}
.seat-switch-lab{font-size:11px;letter-spacing:.1em;text-transform:uppercase;
 font-weight:700;color:var(--seat-ink);opacity:.62}
/* Sign out is next to the switcher now, not floated away from it. */
.seatbar > .seat-switch ~ .seat-ghost{margin-left:0}
.seat-msg{flex-basis:100%;color:var(--seat-warn)}
/* The shift bar (app/staff/seat.js, design/staff/shift-day/DeskDay.dc.html):
 * today's shift as a thin track between its start and end, how far in, and
 * the duty buttons. It takes the middle of the bar; its buttons must not claim
 * the auto margin that holds Sign out at the end. Square, on the seat tokens. */
.seat-day{flex:1 1 280px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;min-width:0}
.seat-day .seat-ghost{margin-left:0}
.seat-shift{display:inline-flex;align-items:center;gap:8px;min-width:0;flex:0 1 360px}
.seat-shift-t{font-size:12px;opacity:.62;font-variant-numeric:tabular-nums}
.seat-shift-track{flex:1 1 120px;min-width:60px;height:6px;background:var(--seat-field);border:1px solid var(--seat-line);overflow:hidden}
.seat-shift-track > span{display:block;height:100%;background:var(--seat-ink)}
.seat-shift-in{font-weight:600;white-space:nowrap}
.seat-break{display:inline-flex;align-items:center;padding:5px 10px;border:1px solid var(--seat-ink);font-weight:600;white-space:nowrap}
/* The team's messages (app/staff/inbox.js, 20261008093000): a button on the bar
 * with the count of what is waiting, and a drawer over the right of the page.
 * Every colour is one of the seat tokens above, for the reason given there. */
.seat-inbox-btn{display:inline-flex;align-items:center;gap:6px}
.seatbar > .seat-inbox-btn{margin-left:auto}
.seatbar > .seat-switch ~ .seat-inbox-btn,.seatbar > .seat-inbox-btn ~ .seat-ghost{margin-left:0}
.seat-badge{min-width:18px;height:18px;padding:0 5px;border-radius:9px;box-sizing:border-box;
 background:var(--seat-warn);color:var(--seat-ground);font-size:11px;font-weight:700;
 display:inline-flex;align-items:center;justify-content:center}
.seat-inbox{position:fixed;top:0;right:0;bottom:0;z-index:60;width:min(420px,100vw);box-sizing:border-box;
 overflow-y:auto;padding:16px 18px 28px;background:var(--seat-ground);color:var(--seat-ink);
 border-left:1px solid var(--seat-line);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif}
.seat-inbox[hidden]{display:none}
.seat-inbox .si-top{display:flex;align-items:center;justify-content:space-between;gap:12px}
.seat-inbox .si-top .seat-ghost{margin-left:0}
.seat-inbox .si-title{margin:0;font-size:20px}
.seat-inbox .si-h{margin:18px 0 8px;font-size:11px;letter-spacing:.1em;text-transform:uppercase;font-weight:700}
.seat-inbox .si-warn{color:var(--seat-warn)}
.seat-inbox .si-note{color:var(--seat-warn);margin:8px 0 0}
.seat-inbox .si-card{border:1px solid var(--seat-line);background:var(--seat-field);padding:10px 12px;margin:0 0 8px}
.seat-inbox .si-card p{margin:0 0 4px;overflow-wrap:anywhere}
.seat-inbox .si-due{border-color:var(--seat-warn);background:var(--seat-ground)}
.seat-inbox .si-due .seat-btn{width:100%;min-height:44px;margin-top:6px}
.seat-inbox .si-body{font-size:15px}
.seat-inbox .si-meta{font-size:12px;opacity:.62;margin:0}
.seat-inbox .si-threads,.seat-inbox .si-convo{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}
.seat-inbox .si-thread{width:100%;text-align:left;display:grid;grid-template-columns:1fr auto;gap:2px 10px;
 min-height:56px;padding:10px 12px;border:1px solid var(--seat-line);background:var(--seat-ground);
 color:var(--seat-ink);font:inherit;cursor:pointer;border-radius:2px}
.seat-inbox .si-name{font-weight:600}
.seat-inbox .si-last{grid-column:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;opacity:.8}
.seat-inbox .si-thread .si-meta{grid-row:1;grid-column:2}
.seat-inbox .si-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--seat-warn);vertical-align:middle}
.seat-inbox .si-new{display:flex;flex-direction:column;gap:4px;margin-top:12px;font-size:12px}
.seat-inbox select,.seat-inbox textarea{font:inherit;color:var(--seat-ink);background:var(--seat-field);
 border:1px solid var(--seat-line);border-radius:2px;min-height:44px;padding:8px 10px;box-sizing:border-box;width:100%}
.seat-inbox .si-back{margin:12px 0 0}
.seat-inbox .si-convo li{max-width:85%;padding:8px 12px;border:1px solid var(--seat-line);display:flex;flex-direction:column;gap:2px;overflow-wrap:anywhere}
.seat-inbox .si-theirs{align-self:flex-start;background:var(--seat-field)}
.seat-inbox .si-mine{align-self:flex-end;background:var(--seat-ink);color:var(--seat-ground);border-color:var(--seat-ink)}
.seat-inbox .si-reply{display:flex;gap:8px;align-items:flex-end;margin-top:12px}
.seat-inbox .si-reply .seat-btn{min-height:44px}
.seat-inbox .si-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
/* The sheet the bar opens, floated over the page rather than inside its grid. */
#seatGate{position:fixed;inset:auto 16px 16px auto;z-index:50;width:min(420px,calc(100vw - 32px));
 max-height:calc(100vh - 32px);overflow-y:auto;padding:22px;
 background:var(--seat-ground);color:var(--seat-ink);border:1px solid var(--seat-line);
 border-radius:2px;box-shadow:var(--shadow)}

/* ── The lock (10 Sep 2026) ──────────────────────────────────────────────────
 *
 * A portal shows its sign-in sheet and nothing else until somebody is behind
 * the session. seat.js sets the attribute; the selector below is what actually
 * hides the surface, so it covers whatever the prototype runtime rendered
 * without this stylesheet or that file knowing the page's shape.
 *
 * `display:none` rather than opacity or a scrim: a blurred surface is still a
 * surface, and the estate's live orders are exactly what a lock is for. It also
 * takes the whole subtree out of the tab order, which a scrim does not.
 *
 * Locked, the sheet stops being a corner note and becomes the page — it is the
 * only thing to do here, so it sits where the reader is already looking. */
/* The body keeps the ground its own page declared — do NOT paint one here.
 * This rule used to say `background:var(--seat-ground,var(--bg))`, and
 * --seat-ground is declared on the bar and the sheet, not on body, so it always
 * took the fallback: brand.css's --bg. In light that is bone and looked right
 * by accident; on a device set to dark it is #14130F, so locking a portal
 * painted the screen near-black around a white sheet. The staff surfaces are
 * single-theme paper, and their own `body{background:var(--desk)}` is already
 * the right answer in both schemes. */
body[data-seat-locked] > *:not(.seatbar):not(#seatGate):not(#seatMark){display:none!important}
body[data-seat-locked] #seatGate{position:static;inset:auto;margin:0 auto;
 width:min(420px,calc(100vw - 32px));max-height:none;box-shadow:none;border-radius:2px}

/* The Family Office lockup above the locked sheet — app/staff/seat.js writes
 * the element, this decides when it is seen.
 *
 * Hidden by default and shown only under the lock, because #seatMark is a
 * child of <body> on a working portal too: unlocked, the sheet is a note in the
 * bottom-right corner over a surface full of live orders, and a brand mark
 * floating beside it would be decoration on a tool. Locked, the sheet is the
 * whole page and the mark is the cover — which is what the lockup is for.
 *
 * It takes the 14vh the sheet used to carry, and the sheet drops to `0 auto`,
 * so the pair is positioned as one block rather than the mark pushing the form
 * a second 14vh down the screen. Matched widths keep their left edges in line.
 *
 * Third in the `:not()` chain above for the same reason .seatbar and #seatGate
 * are: that rule hides every direct child of a locked body, and it hides with
 * !important, so an exemption is the only thing that reaches this element. */
#seatMark{display:none}
body[data-seat-locked] #seatMark{display:block;margin:14vh auto 14px;
 width:min(420px,calc(100vw - 32px))}
/* Centred over the sheet, not aligned to its left edge. The artwork is a
   symmetric composition — the atom sits over the middle of the wordmark, and
   the tagline and the two territories are centred under it — so left-aligning
   it puts its optical centre 90px to the left of the card's, which reads as a
   mark that missed rather than a mark that was placed. */
body[data-seat-locked] #seatMark img{display:block;width:100%;max-width:200px;height:auto;
 margin:0 auto}
@media (max-width:860px){
  body[data-seat-locked] #seatMark{margin-top:8vh}
  body[data-seat-locked] #seatMark img{max-width:168px}
}

/* The sheet's own colours, on a staff palette.
 *
 * signin.js's rules reach for --bg, --line and --muted, and .primary takes its
 * fill from each RESIDENT page's <style>. A staff page declares none of those:
 * it has --paper, --desk and --ink, and it never had a .primary rule because it
 * never had a button of its own. So on the four portals the sheet drew its
 * inputs in the resident palette and its main button as an unstyled <button> —
 * black on grey. Nobody saw it because the sheet only appeared after somebody
 * clicked Sign in; now it IS the page, and the audit sampled it and was right
 * to fail: three colours declared nowhere on the surface showing them.
 *
 * Mapped rather than written out, the way the bar above does it, so the sheet
 * follows whichever of the two palettes the page around it declares.
 *
 * --muted resolves to ink and the elements that used it carry the opacity
 * instead. That is the same trick .seat-who plays and for the same reason: a
 * grey chosen here would be a colour the design system does not have.
 *
 * --bg is the FIELD's ground, not the sheet's. It was mapped to --seat-ground,
 * which is also what the sheet itself is painted in, so the mobile number sat
 * on paper inside paper: `.si input` drew --paper on --paper with a --desk
 * hairline, and what a tester saw under STAFF SIGN-IN was their own number set
 * as a line of text, with no box to say it could be edited. On the resident
 * pages the same three rules never had that problem, because there --bg is bone
 * and a card is --surface — the field is a step BACK from the card. --seat-field
 * is that step on a staff palette, and --bg is what the sheet's rules ask for
 * by name: the field, the OTP boxes and the other door's card, all of which are
 * things to fill in or tap rather than sheet. */
#seatGate{--bg:var(--seat-field);--line:var(--seat-line);--muted:var(--seat-ink)}
#seatGate .si-lab,#seatGate .si-alt,#seatGate .si-paste summary{opacity:.62}
#seatGate .primary{background:var(--seat-ink);color:var(--seat-ground);
 border:1px solid var(--seat-ink);border-radius:2px;padding:11px 17px;
 font:inherit;font-weight:600;cursor:pointer;min-height:44px}
#seatGate .primary[disabled]{opacity:.5;cursor:default}

/* The seat picker the sheet becomes after a sign-in. A row per portal this
 * number is on, with what the seat is doing now, because "taken" is the thing
 * worth knowing before you tap. Rules, not cards: four boxes would read as four
 * decisions when it is one. */
.si-picks{list-style:none;margin:14px 0 0;padding:0;display:flex;flex-direction:column}
.si-pick{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
 padding:13px 2px;border-top:1px solid var(--seat-line);color:var(--seat-ink);
 text-decoration:none;min-height:44px}
.si-picks li:last-child .si-pick{border-bottom:1px solid var(--seat-line)}
.si-pick:hover .si-pick-name{text-decoration:underline}
.si-pick-name{font-weight:600;font-size:15px}
.si-pick-who{font-size:12.5px;opacity:.62;white-space:nowrap}

/* The sign-in sheet — app/signin.js, mounted into each gated page's #gate.
 *
 * THE SPLIT. On four of the five gated pages this sheet IS the page: #app is
 * hidden behind it, and what a resident met was a heading, a field and a button
 * in a 460px card. It is the first screen a new resident sees and it carried
 * none of the service's name.
 *
 * So it is the layout the car-care app already ships (app/sign-in/page.tsx in
 * scotty-car-care): an ink panel with the lockup, the estate line and the
 * figure, beside the form. Nothing in the panel is a control, so a resident
 * cannot lose the form by reaching for the picture. Below 860px the panel folds
 * to one band across the top — the phone has room for a single column and the
 * form is the column worth having.
 *
 * The split is opt-in from the script (`brand: true`), and only the resident
 * pages ask for it. Without it there is no panel and .si-panel is the whole
 * sheet, unpadded, which is what the four staff doors mount into their own
 * card — see #seatGate above.
 *
 * The pages own two things about it and this file owns the rest: #gate's width
 * and margin (a page decides where its own sheet sits) and .primary's fill. The
 * #gate h2/p/input rules they each carried are gone — they were four copies of
 * this file's job, at an ID specificity nothing here could answer. */
.si{position:relative}
.si-panel{min-width:0}
.si-split{display:grid;grid-template-columns:minmax(0,300px) minmax(0,1fr);align-items:stretch}
.si-split .si-panel{padding:32px 34px 34px}

/* The ink half. */
.si-brand{background:var(--ink);color:var(--accent-ink);padding:30px 26px;
  display:flex;flex-direction:column;justify-content:space-between;gap:24px;min-width:0}
.si-lockup{display:flex;align-items:center;gap:10px}
.si-lockup img{flex:0 0 auto;width:44px;height:44px;object-fit:contain;border-radius:2px}
.si-lockup b{font-family:var(--brand-serif);font-weight:600;font-size:21px;line-height:1;
  letter-spacing:-.025em}
.si-lockup span{display:block;margin-top:6px;font-family:var(--brand-sans);font-size:9.5px;
  letter-spacing:.18em;text-transform:uppercase;font-weight:700;opacity:.72}
.si-statement{display:flex;flex-direction:column;gap:10px}
/* The eyebrow is --muted on paper; on ink there is no muted, so it is the panel's
   own ink at the opacity the seat bar uses for the same job. */
.si-brand .si-eyebrow{margin:0;color:var(--accent-ink);opacity:.62}
.si-say{margin:0;font-family:var(--brand-serif);font-weight:600;font-size:30px;line-height:1.08;
  letter-spacing:-.03em}
/* What the service does, in three lines, where the figure would be. See
   brandPanel() in app/signin.js for why there is no figure on this panel. */
.si-assure{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:9px;
  font-size:13px;line-height:1.45;opacity:.78}
.si-assure li{display:flex;gap:9px;align-items:baseline}
.si-assure li::before{content:"";flex:0 0 auto;width:5px;height:5px;border-radius:50%;
  background:var(--accent-ink);opacity:.55;transform:translateY(-2px)}

/* The form half. */
.si-eyebrow{margin:0 0 8px;font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;
  font-weight:700;color:var(--muted)}
.si-panel h2{margin:0 0 8px;font-size:27px;line-height:1.12}
.si-lede{margin:0 0 22px;max-width:48ch;color:var(--muted);font-size:14.5px;line-height:1.6}
/* The sheet's own Scotty is the same icon; this only says where it sits.
   Left-aligned with the heading under it, and no negative margin now that
   there is no plate padding to pull back. */
.si-scotty{margin:0 0 10px}
.si-lab{display:block;font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;font-weight:700;color:var(--muted);margin:0 0 6px}
.si input,
.si textarea{width:100%;padding:11px 12px;border:1px solid var(--line);background:var(--bg);color:var(--ink);font:inherit;margin-bottom:10px;box-sizing:border-box}
.si textarea{resize:vertical;min-height:84px}
.si .primary{width:100%}
/* The line under the button — "Checking…", "That code did not match". The
   resident pages each give .msg a top margin in their own <style>; the staff
   doors do not, and there it sat flush against the button. Said once, here,
   for every sheet, and hidden while empty so the form does not carry a blank
   line's worth of margin before anything has been said. */
.si .msg{margin:10px 0 0}
.si .msg:empty{display:none}
.si-alt{margin:14px 0 0;font-size:13.5px;color:var(--muted)}
/* The one switch on the home step — "I'm under 18" (20261014114100). The
   profile's .profswitch, drawn here because the step is mounted on /basket, /me
   and /orders alike and only /me carries that stylesheet. */
.si-switch{display:flex;align-items:flex-start;gap:12px;margin:14px 0 18px;cursor:pointer}
.si-switch input{position:absolute;opacity:0;width:0;height:0}
.si-switch .track{flex:none;width:44px;height:26px;border-radius:999px;background:var(--tile);border:1px solid var(--line);position:relative;transition:background .15s}
.si-switch .track:after{content:"";position:absolute;top:2px;left:2px;width:20px;height:20px;border-radius:50%;background:var(--surface);transition:left .15s}
.si-switch input:checked+.track{background:var(--accent);border-color:var(--accent)}
.si-switch input:checked+.track:after{left:20px}
.si-switch input:focus-visible+.track{outline:2px solid var(--accent);outline-offset:2px}
.si-switch .words{display:flex;flex-direction:column;gap:2px;font-size:13.5px;color:var(--muted);line-height:1.45}
.si-switch .words b{font-size:14px;font-weight:600;color:var(--ink)}
.si-link{background:none;border:0;padding:0;color:var(--ink);font:inherit;font-size:13.5px;text-decoration:underline;cursor:pointer;min-height:44px}
.si-link[disabled]{opacity:.55;cursor:default;text-decoration:none}
.si-choice{display:grid;gap:8px;margin:4px 0 0}
.si-choice .btn{border:1px solid var(--ink);background:transparent;color:var(--ink);padding:10px 17px;font:inherit;font-weight:600;cursor:pointer;min-height:44px}

/* The other door, as a whole card rather than an underlined half-sentence.
   It was a 14px tap target for the only other way into the service. */
.si-or{display:flex;align-items:center;gap:12px;margin:22px 0 14px}
.si-or i{flex:1;height:1px;background:var(--line)}
.si-or span{font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;font-weight:700;
  color:var(--muted)}
.si-alts{display:grid;gap:8px}
.si-altcard{display:flex;align-items:center;gap:12px;width:100%;min-height:56px;padding:12px 14px;
  border:1px solid var(--line);border-radius:2px;background:var(--bg);color:var(--ink);
  font:inherit;text-align:left;cursor:pointer}
.si-altcard:hover{border-color:var(--ink)}
.si-altcard span{flex:1;min-width:0}
.si-altcard b{display:block;font-size:14px;font-weight:600}
.si-altcard small{display:block;margin-top:2px;font-size:12.5px;color:var(--muted)}
.si-ico{flex:0 0 auto;color:var(--muted)}

/* The six boxes on the code step. There is ONE field under them — see otp() in
   app/signin.js — drawn transparent over the boxes so the lit box is the caret.
   The old single field was one input with .32em tracking, which read as six
   digits to a designer and as one long number to everybody else. */
.si-codehead{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
.si-otp{position:relative;margin:0 0 14px}
.si-otp-slots{display:flex;gap:8px}
.si-otp-slot{flex:1 1 0;min-width:0;height:58px;border:1px solid var(--line);border-radius:2px;
  background:var(--bg);display:grid;place-items:center;color:var(--ink);
  font:600 24px/1 var(--brand-sans);font-variant-numeric:tabular-nums}
/* 16px, not smaller: an input under 16px makes iOS zoom the page on focus, and
   the resident cannot see the boxes it is meant to be filling. */
.si .si-otp-input{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:0;
  background:transparent;color:transparent;caret-color:transparent;text-align:center;
  font-size:16px;letter-spacing:normal}
.si .si-otp-input::selection{background:transparent}
/* The input spans all six boxes, so the page's own focus ring would draw one
   rectangle around the row and the lit box inside it — two indicators for one
   caret. The lit box IS the focus indicator, and it only appears while the
   field has focus, which is the job the ring was doing. */
.si .si-otp-input:focus-visible{outline:none}
.si-otp-slot[data-active]{border-color:var(--ink);box-shadow:0 0 0 3px var(--tile)}
.si-resend{display:flex;align-items:center;justify-content:center;gap:7px;margin:12px 0 0;
  font-size:13.5px;color:var(--muted)}

/* "Finding your home" — the one step that is a wait rather than a question. */
.si-wait{display:flex;gap:6px;margin:4px 0 0}
.si-wait i{width:7px;height:7px;border-radius:50%;background:var(--ink);opacity:.22;
  animation:si-pulse 1.1s ease-in-out infinite}
.si-wait i:nth-child(2){animation-delay:.18s}
.si-wait i:nth-child(3){animation-delay:.36s}
@keyframes si-pulse{0%,100%{opacity:.22}50%{opacity:.7}}
@media (prefers-reduced-motion:reduce){.si-wait i{animation:none}}

/* One column, and the panel as a band. The figure comes with it — small, beside
   the statement — because the estate line is the half of the panel doing the
   work and a resident on a phone should still get it. */
@media (max-width:860px){
  .si-split{grid-template-columns:minmax(0,1fr)}
  .si-brand{flex-direction:row;flex-wrap:wrap;align-items:center;gap:14px;padding:16px 18px}
  .si-brand .si-statement{flex:1 1 180px;gap:4px}
  .si-say{font-size:19px}
  /* The three lines go: a phone band is a band, not a column of copy above the
     form. The lockup and the statement are what a resident needs to know they
     are in the right place. */
  .si-assure{display:none}
  .si-split .si-panel{padding:22px 18px 26px}
  .si-panel h2{font-size:23px}
  .si-otp-slot{height:52px;font-size:21px}
}
@media (max-width:420px){
  .si-otp-slots{gap:5px}
  .si-otp-slot{height:48px;font-size:19px}
}

/* The address lookup on the area gate's address step (app/address-lookup.js).
   Google draws <gmp-place-autocomplete> itself and its shadow DOM is not ours
   to restyle, so this only places it and sizes the two lines under it.
   color-scheme:light keeps the suggestion list legible: the widget reads the
   page's scheme for its own panel, and on the dark theme it renders its
   dropdown on a dark ground that Google's own text colour does not follow. */
/* min-width:0 on the grid and max-width:100% on the widget: a grid child
   defaults to min-width:auto, and Google's element reports an intrinsic width
   wider than the basket's checkout column, so the search field ran out past
   the card's edge on a desktop screenshot (12 Sep 2026 review). */
.si-lookup{display:grid;gap:6px;margin:0 0 14px;min-width:0;max-width:100%}
.si-lookup gmp-place-autocomplete{display:block;width:100%;max-width:100%;min-width:0;box-sizing:border-box;color-scheme:light}
.si-lookup-msg{margin:0;font-size:12.5px;color:var(--muted)}
.si-lookup-note{margin:0;font-size:11.5px;color:var(--muted)}
.si-lookup-note a{color:inherit;text-decoration:underline}
/* The typed address, behind the search rather than beside it. The link that
   opens it is the only thing between the two, and both go when it is open. */
.si-manual-ask{margin:0 0 14px;text-align:left}
.si-manual{margin:0 0 4px}
/* The pin step (app/address-lookup.js §mountMap). The pin is a fixed element in
   the middle of the frame and the map is centred under it on the phone's own
   fix — nothing drags either, which is also why it is positioned here rather
   than drawn by the maps library. Square corners: the frame is a framed object
   on a worksheet, the same as .card, and the design system has no rounded map.
   The height is a phone's: tall enough to place a gate, short enough that the
   buttons under it are on the screen with it. */
.si-map{position:relative;border:1px solid var(--line);background:var(--tile);overflow:hidden}
.si-map-canvas{width:100%;height:260px}
/* The tip of the pin is the coordinate, so the mark hangs ABOVE the centre of
   the frame: its point sits on the middle pixel, not its body. */
.si-map-pin{position:absolute;left:50%;top:50%;width:24px;height:34px;margin:-34px 0 0 -12px;
  pointer-events:none;color:var(--ink);line-height:0}
.si-map-pin svg{display:block;width:24px;height:34px}
.si-map-dot{fill:var(--surface)}
.si-map-note{margin:8px 0 0;font-size:12.5px;color:var(--muted)}
/* The profile's address panel and its roster. The roster is a safety screen —
   who can see this household's orders — so it reads as a list of people rather
   than as a settings row. */
.profaddr{margin:0 0 6px;font-size:15px;line-height:1.45}
.profpeople{display:grid;gap:8px;margin:10px 0 0}
.profperson{display:flex;flex-wrap:wrap;gap:4px 12px;justify-content:space-between;
  align-items:baseline;padding:8px 10px;border:1px solid var(--line);border-radius:10px}
.profperson .sub{margin:0;font-size:12.5px;word-break:break-word}
.si-paste{margin:16px 0 0;font-size:13.5px}
.si-paste summary{cursor:pointer;color:var(--muted)}
.si-paste p{margin:10px 0}

/* Scotty, peeking out from behind a savings card.
 *
 * The placement is the whole idea: he is a child of the card at z-index:-1, so
 * the card's own background paints over his body and the LAYOUT does the
 * cropping rather than the artwork. That is what keeps the catalogue's hard
 * rule — never over a price, a control or a call to action — true by
 * construction instead of by careful nudging. He cannot cover the number,
 * because he is behind the surface the number sits on.
 *
 * This depends on the host not creating a stacking context. .card is a plain
 * background, border and radius here, so z-index:-1 lands behind it; a host
 * that later grows a transform, an opacity below 1, a filter or
 * isolation:isolate will pull him in front of the content. Hence .peek-host is
 * opt-in and named rather than this hanging off .card directly.
 *
 * No bone plate, unlike the hero and the game figure. The plate exists because
 * his near-black jacket disappears into a dark ground, and what clears the card
 * edge here is his head — fur, on the page's own bone. The jacket that would
 * need the plate is the part the card is covering.
 *
 * --peek-drop is how far he sits down into the card, and it is a PERCENTAGE on
 * purpose: a translateY percentage resolves against the element's own height,
 * so the overlap survives every width the surfaces use. A pixel drop suits one
 * of them and beheads him at the rest.
 *
 * 12.4% is the other half of a contract with scripts/build-scotty-peek.py: it
 * cuts the file at CUT, which leaves the rim at 87.6% of the height, so hiding
 * the bottom 12.4% lands the card's top edge exactly there. aspect-ratio has to
 * match the file too, because it is what gives the element a height before the
 * image loads. scripts/scotty-peek.test.mjs fails if the three drift apart. */
.peek-host{position:relative}
.scotty-peek{position:absolute;z-index:-1;bottom:100%;right:18px;
  width:84px;height:auto;aspect-ratio:632/450;display:block;pointer-events:none;
  transform:translateY(var(--peek-drop,12.4%));
  filter:drop-shadow(0 3px 6px rgba(11,11,12,.18))}
.scotty-peek.left{right:auto;left:18px}
/* Same approved peek artwork, three degrees of hiding. `shy` leaves mostly the
 * face above the edge; `bold` lets the paws read. These are crops/placements,
 * not new character poses, so Scotty stays on-model and one source asset can be
 * replaced later without hunting down derivative artwork. */
.scotty-peek.shy{width:70px;--peek-drop:31%}
.scotty-peek.bold{width:96px;--peek-drop:7%}

/* Narrow screens stack the tiles and the summary rows, which puts another card
 * directly over the band the peek occupies — he is behind the card plane, so a
 * sibling in front of him leaves nothing but ear tips in the gap. Surfaces
 * where that happens pass hideOnSmall; the rest keep him. */
@media (max-width:560px){
  .scotty-peek{width:70px;right:12px}
  .scotty-peek.left{left:12px}
  .scotty-peek.peek-sm-hide{display:none}
}
/* The lift has to carry the drop with it — a bare translateY(-3px) would drop
 * the offset and shove him up off the card. */
@media (prefers-reduced-motion:no-preference){
  .scotty-peek{transition:transform .18s ease}
  .peek-host:hover .scotty-peek{transform:translateY(calc(var(--peek-drop,12.4%) - 3px))}
}

/* ── Design review, 17 Sep 2026 ─────────────────────────────────────────────
 * Money figures take the brand serif, the way the ETA and the movers board
 * already do — the totals are the figures a resident opens these pages for.
 * .sum.tot is the "To pay" / grand-total row on /basket, /orders and the
 * slip cards; nothing smaller changes. */
.sum.tot .amt{font-family:var(--brand-serif);font-weight:600;font-size:17px;letter-spacing:-.01em}
/* The pilot pill gives up its room before the nav has to fold: between 560 and
 * 760 it was what pushed the row into a wrap on /shop and /orders. It is
 * metadata, and the Menu breakpoint below already drops it. */
@media(max-width:760px){.mast .pill{display:none}}

/* ── Shared controls (review consolidation, 17 Sep 2026) ────────────────────
 * These lived as near-identical copies in each page's <style> and had started
 * to drift: .chip was 36px on the shelf and 40px in the naming panel, and two
 * pages had lost the 44px floor on .primary. One spec each, here, where the
 * palette already lives.
 *
 * READ THIS BEFORE ADDING A VARIANT. Every page links brand.css AFTER its own
 * <style>, so at equal specificity these rules are the ones that apply — a
 * page cannot override them by redeclaring `.ghost`. A page that needs its own
 * takes a descendant selector (`.wrap .ghost`, `.addr .ghost`) or a modifier
 * (`#gate.narrow`), both of which outrank a bare class wherever they sit. The
 * one sheet that can answer at equal specificity is shop.html's, at the foot
 * of its <body>, which is why the 44px phone chip still works from there. */
.primary{background:var(--accent);color:var(--accent-ink);border:1px solid var(--accent);
 border-radius:2px;padding:10px 17px;font-weight:600;cursor:pointer;min-height:44px}
.primary[disabled]{opacity:.5;cursor:default}
.ghost{background:var(--surface);color:var(--ink);border:1px solid var(--line);border-radius:2px;
 padding:10px 17px;font:inherit;font-weight:600;cursor:pointer;min-height:44px}
.ghost:hover{border-color:color-mix(in srgb,var(--accent) 45%,var(--line))}
.ghost[disabled]{opacity:.5;cursor:default}
.chip{display:inline-flex;align-items:center;border:1px solid var(--line);background:var(--surface);
 color:var(--ink);border-radius:2px;padding:7px 13px;font:inherit;font-size:13px;cursor:pointer;min-height:40px}
.chip[aria-pressed="true"]{background:var(--accent);border-color:var(--accent);color:var(--accent-ink);font-weight:600}
/* The sign-in sheet's frame. Unpadded and clipped because the ink panel bleeds
 * to the card's edge — that is /basket, /me, /orders, /shop and /upload. On
 * /chat and /swaps the sheet is a plain form with no panel beside it, and it
 * says so: #gate.narrow, an id and a class, which outranks the bare id. */
#gate{padding:0;overflow:hidden;max-width:880px;margin:40px auto}
#gate.narrow{padding:24px;overflow:visible;max-width:460px}

/* ── Page shell + pressable chips (review 2, 29 Sep 2026) ───────────────────
 * The .wrap/h1/h2/.lede trio had a copy in every page <style> and three
 * different .lede margins. One spec here; a page that wants a different
 * measure takes a descendant selector. h1/h2 already take the serif above.
 * Pressable chips on /me and /chat (.profchip, .filter, the composer's
 * order <select>) were the last 999px controls in a 2px system — status tags
 * (.proftag, .state, .cap .tag) stay pills, they are not pressed. */
.wrap{max-width:1020px;margin:0 auto;padding:30px 18px 90px}
h1{font-size:27px;margin:0 0 6px}
h2{font-size:17px;margin:0 0 4px}
.lede{color:var(--muted);margin:0 0 18px;max-width:70ch}
.profchip,.filter,.about select{border-radius:2px}
/* Captions a resident reads to follow the thread — who spoke, which run, the
 * run's own notes — are content, not labels: 12px floor (review 1 global 2). */
.cap,.stamp,.sys{font-size:12px}
.cap .tag{font-size:11px}

/* ── Scottie's subbar (review 3, 29 Sep 2026) ───────────────────────────────
 * Was pasted into match/search/play/movers; one copy. The strip scrolls under
 * 560 with a right-edge fade so the fourth tab is seen to exist. The .note
 * slot stays styled for a page that has one sentence worth saying; the
 * shared "shelf prices, open to anyone" line is gone — each lede says it. */
.subbar{background:var(--bg);border-bottom:1px solid var(--line)}
.subbar .in{max-width:1020px;margin:0 auto;padding:14px 18px;display:flex;gap:18px;align-items:center;flex-wrap:wrap}
.subkick{display:flex;gap:8px;align-items:center}
.subkick img{width:24px;height:24px;object-fit:contain}
.subkick b{font-size:11px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
.subbar .note{font-size:12.5px;color:var(--muted);margin-left:auto}
.subtabs{display:flex;border:1px solid var(--ink);border-radius:2px;overflow:hidden}
.subtabs a{display:inline-flex;align-items:center;min-height:44px;padding:0 18px;font-size:13px;font-weight:600;
 text-decoration:none;background:var(--surface);color:var(--ink)}
.subtabs a + a{border-left:1px solid var(--ink)}
.subtabs a[aria-current]{background:var(--accent);color:var(--accent-ink)}
.subtabs a:hover{text-decoration:none}
@media(max-width:560px){
  .subbar .in{gap:12px}
  .subbar .note{flex-basis:100%;margin-left:0}
  .subtabs{max-width:100%;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;
    mask-image:linear-gradient(90deg,#000 calc(100% - 28px),transparent);
    -webkit-mask-image:linear-gradient(90deg,#000 calc(100% - 28px),transparent)}
  .subtabs a{padding:0 14px;white-space:nowrap;flex:0 0 auto}
}

