/* ============================================================================
   V2 — the Aug 31 redesign. Began at tablet Active Orders; now the whole app.

   Source of truth: docs/design/aug31-ui-layouts/ (screen `7a`). The handoff
   README's token table is authoritative EXCEPT for the accent: it lists blue
   #3b6fd4, but that colour appears only in the superseded 5d/6a/6b screens.
   Every accent in 7a/7b/7c is teal #2a7d94, and 7a supersedes. Teal it is.

   WHY A SEPARATE SHEET rather than swapping the tokens in :root — the DESIGN
   covers Active Orders, Customers and the phone screens, and nothing else. It
   has never covered Reports, Teams, Admin, Settings or the twenty modals, and
   those were full of literal rgba() picked against a DARK background.
   Rewriting :root would have flipped the app to paper and left every
   undesigned surface illegible, in one diff nobody could review.

   Reports, Teams, Admin and the modals HAVE since been extended from the same
   system — their sections are at the foot of this file. They are still not in
   the handoff, so this sheet, not the design, is their source of truth.

   TWO SCOPES, and the difference is structural rather than stylistic:
     `#app.v2`     the operator board and its panes — everything inside #app.
     `body.v2-on`  the five customer-facing pages AND all twenty modals, which
                   sit OUTSIDE #app in the markup and so cannot be reached by
                   the selector above.
   shared.js sets both, on every page.

   ⚠️ `body.v2-on` is on the operator app's <body> too, so a rule written for
   the public pages' primitives (.btn, .card, .form-input) also lands on the
   operator app's identically-named ones. That is deliberate — same controls,
   one set of rules — but it means no rule here is only a "public page" rule.

   HOW IT LOADS: a <link> AFTER index.html's inline <style>. Nearly every rule
   here ties the shipped one on specificity (#id + .class) and wins on source
   order alone. Keep the link last, and keep these selectors at or above the
   specificity of the rule they replace.

   ⚠️ LAYOUT LANES — the trap this file keeps falling into. There are FOUR
   contexts, and no one media query reaches them all:
     rail             coarse+landscape, or hover+>=1000px  (tablet landscape, desktop)
     phone            max-width: 640px
     phone-land       the .phone-land class, set by app.js
     tablet portrait  coarse+portrait+>=641px — an iPad held upright
   The last one was added late, after the scan slab was found sitting there in
   the DARK theme's blue: the shipped slab's display gate has no orientation
   clause, so the bar was up in a layout no v2 rule reached.

   The lesson, stated once because it has now bitten repeatedly: anything that
   decides whether a thing is VISIBLE or LEGIBLE — a colour, a glyph swap,
   `display: flex` on a control — belongs OUTSIDE all four lanes. Only geometry
   goes inside one. See the "colour that must hold at every width" and
   "structural" blocks below, which exist for exactly this.

   ⚠️ DEPLOY: v2.css is already in sw.js ASSETS and live — it is no longer a
   new file, so the upload-before-sw.js rule has done its job. What still bites
   is that the <link> carries NO version query: the CACHE string in sw.js is
   the only thing tying a browser to the current copy. Bump it on every change
   to this file, or holders of an old copy render new markup against old rules.
   ========================================================================= */

/* ---- BASE RULES (deliberately NOT scoped to .v2) -------------------------
   These four elements were added to index.html/app.js to serve v2, but they sit
   in the shared markup, so the SHIPPED theme has to be told about them too. An
   unsized <svg> defaults to 300x150 — without the sizing rule below, turning v2
   off would blow the scan bar and every action square apart. Scoping these to
   .v2 would be the tidier-looking mistake. */
.scan-slab-qr { display: none; }
.rail-stats { display: none; }
/* `.rail-brand img` in the shipped landscape block is (0,1,1) and outranks a
   bare `.rail-mark`, so the v2 mark rendered UNDER the shipped lockup — two
   marks stacked. Match its specificity to actually win. */
.rail-brand .rail-mark, .rail-brand .rail-words { display: none; }
.order-act-ico { width: 18px; height: 18px; display: block; }
/* Sized by HEIGHT: the path's box is tight to the ink, so 11px here is 11px of
   cap height — the same optical weight the 15px/700 text carried — and the
   width follows the ratio to ~123px inside the rail's 137px word column.
   Unscoped for the reason above: an unsized <svg> is 300x150. */
.rail-wordmark { display: block; height: 11px; width: auto; max-width: 100%; }
/* The symbol's path is `fill="currentColor"` like every other icon in the
   sprite, so the wordmark takes whatever `color` it inherits — and the colour
   it would inherit from the rail is WHITE. Pinned here, UNSCOPED, rather than
   in the rail block: a colour that decides whether the mark is visible at all
   must not be reachable only inside a media query. */
#app.v2 .rail-wordmark { color: var(--ink); }
#app.v2 .rail-brand .rail-mark-ink { display: block; width: 34px; height: 34px; flex: none; color: var(--ink); }

/* The same swap in the HEADER, and the reason it is here rather than in a
   layout block: between 641px and 999px neither the rail block nor the phone
   block reached the header, so logo.png rendered raw — a blue tile beside a
   WHITE wordmark on paper, i.e. nothing. That band is the one the sweep exists
   to catch. Colour is not layout, so this is stated once, for every width, and
   the rail block hides the header brand where the rail carries it instead. */
#app.v2 .brand img[src="logo.png"] { display: none; }
/* st-mark.png is a blue tile with an OPAQUE white ST, so unlike logo.png (whose
   ST is a transparent knockout) no filter reaches it — brightness(0) would give
   a solid black square. #ico-st-mark is the same tile traced with the ST as an
   evenodd HOLE, so the paper behind shows through and the mark sits on whatever
   ground it lands on: --paper in the header, --paper-2 in the rail. */
#app.v2 .brand-mark-ink { display: block; width: 26px; height: 26px; flex: none; color: var(--ink); }
#app.v2 .brand-wordmark {
  display: block; height: 12px; width: auto; max-width: 100%;
  color: var(--ink); flex: none;
}

/* ---- COLOUR THAT MUST HOLD AT EVERY WIDTH --------------------------------
   Found by sweeping widths rather than by looking: between 641px and 999px the
   app falls through BOTH v2 layout blocks — the rail wants >=1000px and the
   phone block wants <=640px — so it got v2's paper ground with none of v2's
   colour corrections. Gold text on paper at 1.47:1, in the stats bar, the queue
   label, the count badge and every hollow on the board.
   That range is not hypothetical: an iPad in portrait is 768-834px.

   Colour corrections do not belong in a LAYOUT media query. These are the ones
   that were trapped in one. Geometry stays per-layout below. */
#app.v2 .stat-value.gold { color: var(--ink); }
/* Reports colours every money figure gold with an inline `color: var(--gold)`,
   which the rail lane overrides to ink — money is the SUBJECT of that screen, so
   colouring it gold says nothing and costs the contrast. That override sat in the
   rail lane, so tablet portrait rendered Billed, Team Revenue and Service Revenue
   in gold on paper at 1.47:1, against the 3:1 those 25px figures need.

   The same 1.47:1 this block's own header cites. It was missed twice: by the
   original width sweep, and then by a sweep that hunted the DARK palette's
   literals — gold is not one of them, it is a v2 token, legitimate on the Scan
   bar and nowhere else. Contrast is what catches this, not palette membership.
   Re-swept every pane by contrast afterwards; this was the only lane-specific
   failure, so it is the last of them.

   The rail lane keeps its own copy: identical selector and value, so it is
   redundant rather than wrong, and leaving it avoids re-ordering a working rule. */
#app.v2 #reportCards div[style*="var(--gold)"],
#app.v2 #reportSections div[style*="var(--gold)"] { color: var(--ink) !important; }
#app.v2 .order-hollow { color: var(--ink); }
#app.v2 .queue-label { color: var(--text-secondary); }
#app.v2 .count-badge { background: var(--ink); color: var(--paper); }
#app.v2 .ready .count-badge { background: var(--success); color: var(--paper); }

/* The scan slab is the same lesson again, and the most visible instance of it.
   The shipped button's blue tint is painted inside `(pointer: coarse) and
   (hover: none)` — which has NO orientation clause, so an iPad in portrait
   matches it and the bar IS up — while every one of v2's gold overrides sits
   in a layout block that portrait misses (the rail wants >=1000px, the phone
   bar wants <=640px, phone-land wants a landscape phone). So the one control
   the counter reaches for most rendered dark-theme blue on a paper board, with
   the shipped camera lens in place of the QR glyph.

   Paint, glyph and the ink edge, once, for every width. Each layout still
   places and sizes the bar below; phone-land re-states `border` because its
   edge is on the LEFT. The QR glyph carries a size here on purpose — this is
   the rule that turns it on, and an unsized <svg> is 300x150. */
#app.v2 .scan-slab { background: none; padding: 0; }
#app.v2 .scan-slab-btn {
  background: var(--gold); color: var(--ink);
  border: none; border-top: 2px solid var(--ink);
  font-family: 'Space Grotesk', sans-serif;
}
#app.v2 .scan-slab-btn:hover,
#app.v2 .scan-slab-btn:active { background: var(--gold-hover); transform: none; }
#app.v2 .scan-slab-lens { display: none; }
#app.v2 .scan-slab-qr { display: block; width: 32px; height: 32px; flex: none; }
#app.v2 .scan-slab-title { text-transform: uppercase; letter-spacing: 0.14em; }
#app.v2 .scan-slab-hint { color: var(--gold-ink); opacity: 1; }

/* ---- THE BOARD'S PAINT, EVERY WIDTH --------------------------------------
   The same sweep, applied to the queue itself. The rail lane and the phone
   lane had each written the board's paint INSIDE their own media query, and
   checked declaration by declaration they had written the SAME values. Only
   sizes and spacing differed. So tablet portrait, matching neither, kept the
   shipped dark-theme paint on a paper board: cards with a fill and a full
   border instead of ruled rows, a teal pill for the sort thumb, and — the two
   that were simply the dark palette showing through — a GREEN action square
   (rgba(77,200,122,…)) and a CYAN team badge (rgba(77,184,212,…)).

   ⚠️ WHAT BELONGS HERE, precisely: a declaration both designed lanes make with
   the SAME value. That test is not a formality — applied by eye it let four
   through that only ONE lane made (the thumb's radius, the queue label's
   tracking, the card's `border: none`, the badges' uppercase), and each of
   those silently changed the phone. Where the lanes disagree, the value was
   never global; it goes in a lane. The intersection is what is safe to hoist.
   ------------------------------------------------------------------------ */
#app.v2 .order-card { background: none; border-bottom: 1px solid var(--line-soft); }
#app.v2 .order-card:hover { background: #eeebe3; }
/* The shipped `.ready` row tints its BORDER green (rgba(77,200,122,0.3)) as
   well as its fill. The rail kills it with `border: none`; the phone lane only
   drops the left border, so the green survived on a phone's top edge. Colour
   only here — the tint is neutralised without touching border WIDTHS, so no
   lane's geometry moves. */
#app.v2 .order-card.ready { background: none; border-color: var(--line-soft); }
#app.v2 .order-name { font-size: 17px; font-weight: 500; color: var(--ink); }
#app.v2 .order-hollow {
  font-weight: 700; color: var(--ink); font-family: 'Space Grotesk', sans-serif;
}
#app.v2 .order-meta { font-size: 10px; text-transform: uppercase; }
/* Ink with a paper glyph in both lanes; only the square's SIZE is a lane's
   business (44px at the rail, 52px under a thumb). */
#app.v2 .order-act { background: var(--ink); color: var(--paper); border: none; padding: 0; }
/* Outlined, never filled — a badge is a label on the row, not an object on it.
   The two lanes pick different border colours, so only the shared voice moves. */
#app.v2 .order-team-badge, #app.v2 .order-walkin-badge {
  font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.1em;
  padding: 2px 6px; background: none;
}
/* .sort-thumb is the one element exempted from the blanket radius reset at the
   top of this file, so it keeps a shipped 18px pill AND a teal fill unless a
   rule says otherwise. Both lanes kill the teal, so the FILL is hoisted. The
   RADIUS is not: the rail squares the thumb, the phone leaves it a pill.
   !important because the exemption is what it fights. */
#app.v2 .sort-thumb { background: var(--ink) !important; }
#app.v2 .sort-opt {
  padding: 4px 9px; font-size: 9px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--text-secondary);
}
#app.v2 .sort-opt.active { color: var(--paper); }
/* The lane panels. `.has-items` paints a literal rgba() tint — gold on WAITING,
   green on READY — right in the dark theme and wrong on paper twice over: the
   green is the dark palette showing through, and gold in this system is the
   Scan surface and NOTHING else. Both lanes remove it, the phone's comment
   saying so outright. Addressed on the .has-items compound because that is the
   selector doing the painting; an ID still outranks its three classes. */
#app.v2 .queue-card { border: none; padding: 0; }
#app.v2 .queue-card.waiting.has-items,
#app.v2 .queue-card.ready.has-items { background: var(--paper); border: none; }

/* ---- THE ACCOUNT HOLDER'S CARD, EVERY WIDTH ------------------------------
   MARKED, not shaded. This was already reasoned out and fixed — and then left
   inside the rail lane, so tablet portrait and phones never got it and kept the
   SHIPPED card instead: `rgba(45,63,82,0.4)`, a dark blue-grey slab with a cyan
   edge, sitting on a paper panel. Its labels measured 1.81:1 there. That dark
   block is the "grey highlight" it looked like.

   The reasoning, unchanged: the paper-3 fill it replaced was only 1.11:1 from
   the page — a tonal step buying almost no separation, reading as a highlight
   that had failed. A 2px ink left edge is how this system says "this one", the
   same device the wheel-side extras block uses, and it keeps the card in the
   same tonal family as everything around it.

   ⚠️ The labels go DARKER here, not lighter. White is the instinct carried over
   from the dark theme, where the card really is a dark slab; on paper it would
   be 1.25:1. #57534a on paper is 6.85:1. The direction inverts with the ground. */
#app.v2 .primary-card {
  background: none; border: none; border-left: 2px solid var(--ink);
  padding: 14px 16px 14px 14px;
}
#app.v2 .primary-card .pref-label { color: #57534a; }

/* ---- THE ROSTER'S POSITION CHIPS -----------------------------------------
   Neither chip had a v2 rule, so both wore the shipped dark-theme treatment on
   paper: the goalie was `var(--gold)` on a gold wash — the 1.47:1 pairing this
   sheet already outlaws everywhere else — and the player was v2's teal sitting
   on the dark theme's CYAN wash, rgba(77,184,212,0.1).

   Both become outlined mono chips, which is what every other chip in this system
   already is (.visit-paid, .qf-chip, .order-team-badge): no tinted fill, a 1px
   edge, square.

   Only the GOALIE is chipped at all now. A roster runs about two goalies to
   fifteen players, so the "Player" chip labelled the state you would assume and
   left the goalie hiding among fourteen identical neighbours. Absence means
   player, the same convention the walk-in badge uses. The accent marks the
   exception; nothing marks the default. Accent on paper is 5.31:1; the gold this
   replaced was 1.40 — the pairing this sheet outlaws everywhere else.

   ⚠️ NOT a filled chip. A filled ink chip means active/selected in this system
   (.qf-chip.active, .sort-thumb, .view-tab.active) and roster rows are THEMSELVES
   selectable — .player-row.selectable / .selected, for picking who dropped skates
   off. A filled goalie chip would read as "this player is selected". */
#app.v2 .pos-chip {
  background: none !important; border-radius: 0 !important;
  font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.08em;
  text-transform: uppercase; padding: 2px 6px;
}
#app.v2 .pos-goalie { border: 1px solid var(--accent); color: var(--accent) !important; }

/* ---- THE CUSTOMER ROW'S TWO CONTROLS, EVERY WIDTH ------------------------
   Found by sweeping the other panes for dark-palette literals rather than by
   looking at them: Teams and Reports came back clean, Customers did not. The
   balance badge keeps a green wash (rgba(77,200,122,0.1)) and the quick-sharpen
   square a cyan one (rgba(77,184,212,0.08) on a rgba(77,184,212,0.3) border) —
   both straight from the dark theme, sitting on paper.

   ⚠️ This one is NOT portrait-only, which is why it is here and not in a lane:
   the phone lane never addressed either control, so a phone shows the same two
   washes today. Only the rail lane ever fixed them.

   Hoisting still passes the two-lane test, just not with the pair used above:
   the RAIL and PHONE-LAND both convert the badge to the same outlined mono
   chip, and phone-land hides the sharpen square rather than painting it, so the
   rail's outline is the only v2 value anyone has written for it. Sizes stay in
   the lanes — 36px against a cursor, 44px under a thumb. */
#app.v2 .balance-badge {
  font-family: 'DM Mono', monospace; letter-spacing: 0.08em; text-transform: uppercase;
  background: none; border: 1px solid var(--line-chip); color: var(--text-secondary);
}
#app.v2 .balance-owed { border-color: var(--danger); color: var(--danger); }
#app.v2 .quick-sharpen-btn {
  background: none; border: 1px solid var(--ink); color: var(--ink);
}
#app.v2 .quick-sharpen-btn:hover { background: var(--ink); color: var(--paper); }

/* ---- THE TEAM FILTER, EVERYWHERE -----------------------------------------
   Same lesson as the colour block below: the chips had been styled only inside
   the phone layouts, so on desktop they kept the shipped tinted pill. Shape and
   colour are not layout — they belong at every width. Only the bar's PLACEMENT
   changes per layout (its own row on the board, the phone's status strip, the
   wheel's side panel). */
#app.v2 .queue-filter-bar { gap: 6px; margin-bottom: 12px; }
#app.v2 .qf-chip {
  flex: none; font-family: 'DM Mono', monospace; font-size: 10px;
  letter-spacing: 0.1em; text-transform: uppercase; padding: 4px 9px;
  border-radius: 0; background: none;
  border: 1px solid var(--line-chip); color: var(--text-secondary);
  white-space: nowrap; font-weight: 400;
}
#app.v2 .qf-chip:hover { border-color: var(--ink); color: var(--ink); }
#app.v2 .qf-chip.active {
  background: var(--ink); border-color: var(--ink); color: var(--paper);
}

/* ---- STRUCTURAL: controls that must be a flex box at EVERY size ---------
   The shipped stylesheet declares `display: flex` for these inside the rail
   media query, so any layout outside it — phone portrait, phone landscape —
   got a plain block box and every flex property written for it silently did
   nothing. The symptom is always the same: the icon pinned to the left edge
   while the label centres itself, which reads as "slightly off" rather than as
   broken, so it survives review.

   It has bitten four times on this branch: the scan slab button, .tab-ico, the
   portrait tab buttons, and the quick-sharpen square. Declared once here,
   outside every media query, so the next control added to a small layout
   inherits a working box instead of the fifth instance of this bug.

   Direction and gaps stay per-layout; only "this is a flex box, centre its
   contents" is global. */
#app.v2 .quick-sharpen-btn,
#app.v2 .order-act,
#app.v2 .order-cancel,
#app.v2 .skater-zap {
  display: flex; align-items: center; justify-content: center;
}
/* .view-tab gets the flex box but NOT the centring. Those four are icon-only
   squares where centring is always right; a rail row is an icon FOLLOWED BY a
   label, and centring the pair made every row start at a different x — 53, 63,
   77 — so nothing lined up. Each layout says how it packs: the rail from the
   left, the phone bar stacked and centred. */
#app.v2 .view-tab { display: flex; align-items: center; }
/* Icon-only buttons (the ⋯ kebabs) are a square with a glyph in the middle, not
   a text button that happens to be narrow. They were inheriting the 14px/20px
   padding written for the PUBLIC pages' .btn — `body.v2-on` matches inside the
   operator app too — which made them 42x51 with the glyph on a text baseline,
   sitting low and left of centre.
   inline-flex, square, no padding: the box centres the glyph instead of the
   glyph's own metrics deciding where it lands. */
body.v2-on .btn-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; padding: 0; line-height: 1; flex: none;
}

#app.v2 {
  /* ---- the design's palette, in the shipped app's token names -------------
     Overriding the EXISTING names (rather than inventing a parallel set) is
     what makes this a re-skin instead of a rewrite: every inline
     `color:var(--text-secondary)` that app.js already emits follows along
     without app.js being touched at all. */
  --ink:        #16150f;
  --paper:      #f4f2ed;
  --paper-2:    #efece5;   /* rail, side panels */
  --paper-3:    #e9e6de;   /* column headers, status strips */
  --line:       #d8d4ca;   /* 1px panel rules */
  --line-soft:  #e2ded4;   /* list row rules */
  --line-chip:  #cfcabd;   /* neutral chip borders */
  --v2-hover:   #e6e2d8;
  --gold-hover: #f0d668;
  --gold-ink:   #5f531c;   /* text ON gold */

  --accent:          #246c80;
  --accent-bright:   #16150f;
  --gold:            #e8c84a;
  --success:         #2f6b45;
  --danger:          #c8352a;
  --text-primary:    #16150f;
  --text-secondary:  #57534a;
  /* Darkened from #8a857a (2026-09-01). The old value measured 3.28:1 on
     --paper, 3.11 on --paper-2, 2.95 on --paper-3 and 2.84 on the row-hover
     ground — failing AA's 4.5:1 for small text on every surface it lands on,
     and it lands on a lot: 25 rules here and 92 inline colours in app.js all
     resolve through this one token. This value clears 4.5:1 on ALL FOUR
     grounds (worst 4.62, on hover), at the same hue and saturation, and stays
     7 points lighter than --text-secondary so the three-tier hierarchy holds. */
  --text-muted:      #67635b;
  --border:          #d8d4ca;
  --border-hover:    #16150f;
  --surface:         #f4f2ed;
  --card-bg:         #efece5;
  --steel:           #efece5;   /* the scan slab's gradient base */
  /* The rest of the dark palette. app.js writes these into INLINE styles (the
     detail-panel avatar is `background: var(--steel-light)`), and an inline
     style cannot be reached by a stylesheet — so any token left un-remapped
     shows up as a dark patch on a paper screen. Remap the whole set, not just
     the ones the board happened to need. */
  --steel-mid:       #e9e6de;
  --steel-light:     #e2ded4;
  --ice:             #f4f2ed;
  --ice-mid:         #e9e6de;
  --ice-deep:        #246c80;
  --blade:           #57534a;

  background: var(--paper);
  color: var(--ink);
  font-family: 'Space Grotesk', 'Geom', Helvetica, sans-serif;
}

/* The viewer paints behind #app, so the shell has to change with it or the
   paper board floats on the old midnight blue. */
body.v2-on { background: #f4f2ed; }

/* Radius 0 everywhere inside the UI — the design separates by 1px rules and
   paper tones, never by rounded cards or shadows. One blanket rule beats
   chasing forty individual border-radius declarations, and anything that
   genuinely needs a curve can opt back in below. */
#app.v2 *:not(.sort-thumb) { border-radius: 0 !important; box-shadow: none !important; }

/* Mono is always uppercase with wide tracking in this system. */
#app.v2 .count-badge,
#app.v2 .queue-label,
#app.v2 .order-meta,
#app.v2 .sort-opt { font-family: 'DM Mono', monospace; }

/* ---- NAV RAIL ----------------------------------------------------------
   Same media condition as the shipped rail, verbatim, so v2 turns on exactly
   where the rail does and the two can never disagree about what "tablet"
   means. */
@media (pointer: coarse) and (hover: none) and (orientation: landscape),
       (hover: hover) and (min-width: 1000px) {

  /* A fixed-height shell, like the 1240x780 the design is drawn at: the page
     itself never scrolls, and each pane scrolls inside its own columns. Row 1
     is the header, row 2 takes ALL the slack and holds whichever pane is up,
     so nothing here has to guess how tall the header or a title row is.
     ⚠️ Known limit: the notice bars (#stickerStockBar, #pendingBar) would take
     row 2 when they appear and push the pane into an auto row that clips.
     They are unstyled in v2 anyway — both are on the list with Teams and
     Reports. */
  #app.v2 {
    --rail-w: 222px; --rail-gap: 0px; --v2-scan-h: 82px;
    padding: 0 0 var(--v2-scan-h);
    height: 100vh; height: 100dvh; overflow: hidden;
    align-items: stretch;
    grid-template-rows: auto minmax(0, 1fr);
  }
  #app.v2 > #customersView, #app.v2 > #teamsView { display: flex; flex-direction: column; min-height: 0; }

  /* The rail is a filled column with a hard ink edge — in this system it is a
     panel, not the hairline the dark theme demoted it to. */
  #app.v2 > .view-tabs {
    background: var(--paper-2);
    border-right: 1px solid var(--ink);
    padding: 0;
    top: 0; height: 100vh; height: 100dvh;
    gap: 0;
  }

  /* Brand block: 2px ink rule under it, the heaviest line in the rail. */
  /* logo.png is a lockup whose wordmark is WHITE — on paper it vanishes and the
     rail reads as a lone blue monogram. v2 builds the lockup instead: the mark
     alone, then the name and the shop in ink. */
  #app.v2 .rail-brand {
    display: flex; align-items: center; gap: 11px;
    padding: 20px; border-bottom: 2px solid var(--ink);
  }
  #app.v2 .rail-logo { display: none; }
  /* The rail carries the lockup at these widths, so the header must not repeat
     it. The shipped `.header .brand img` hide is (0,2,0) and loses to the
     unscoped `#app.v2` pair above — this restates it at matching weight, and
     unlike those, WHICH element carries the brand really is a layout question. */
  #app.v2 .brand-mark-ink, #app.v2 .brand-wordmark { display: none; }
  #app.v2 .rail-brand .rail-mark { display: none; }
  #app.v2 .rail-brand .rail-words { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
  /* The name is the drawn wordmark, not the UI font — #ico-wordmark is the
     lockup's own lettering traced to vector, so it takes ink through
     `currentColor`. Its sizing is unscoped, up with the other svg sizing. */
  #app.v2 .rail-name { display: block; }
  #app.v2 .rail-shop {
    font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.14em;
    text-transform: uppercase; color: var(--text-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  #app.v2 .rail-shop:empty { display: none; }

  /* Left-justified, with the icons in a fixed slot so every label starts on the
     same edge whatever the glyph. The svg's own viewBox centres the artwork
     inside that slot, so "centred icon, left-aligned text" needs no wrapper. */
  #app.v2 .view-tab {
    justify-content: flex-start; text-align: left;
    padding: 12px 18px; gap: 11px;
    border-left: 3px solid transparent;
    font-size: 14px; font-weight: 500;
    color: var(--text-secondary); background: none;
  }
  #app.v2 .view-tab .tab-ico { width: 20px; height: 20px; flex: none; }
  #app.v2 .view-tab:hover { background: var(--v2-hover); }
  /* Active is a solid ink fill with paper text — the one place in the design
     that inverts, which is what makes it findable at a glance from arm's
     length at the counter. */
  #app.v2 .view-tab.active,
  #app.v2.pane-orders #tabOrders {
    background: var(--ink); color: var(--paper); border-left-color: var(--ink);
  }
  /* border-left-color too, and it is the one that was missed. `.view-tab` carries
     a 3px transparent stripe that the active rule fills with ink; clearing only
     the background and the colour left that stripe lit on whichever tab
     switchView had last marked .active, so standing on the board drew a stray
     ink bar beside Customers. Invisible while the rail sat 200px inside the
     bezel — obvious the moment the shell started filling the screen. */
  #app.v2.pane-orders .view-tab.active {
    background: none; color: var(--text-secondary); border-left-color: transparent;
  }
  #app.v2 #tabAdmin, #app.v2 #tabAdmin.active { color: var(--gold-ink); }
  #app.v2 .view-tabs > .view-tab:first-of-type { margin-top: 14px; }
  /* Settings sits apart — pinned to the foot with a rule above it — but its
     LEFT padding has to match the rows above or its icon and label sit 2px off
     the column everything else shares. Vertical padding is its own. */
  #app.v2 #tabSettings {
    margin-top: auto; border-top: 1px solid var(--line);
    padding: 16px 18px; font-size: 13px;
  }
  #app.v2 .tab-ico { opacity: 1; }

  /* ---- CONTENT HEADER -------------------------------------------------- */
  #app.v2 > header.header {
    margin: 0; padding: 18px 24px;
    border-bottom: 1px solid var(--line);
  }
  #app.v2 .header-search {
    max-width: 340px;
    background: #fbfaf7; border: 1px solid var(--ink);
  }
  #app.v2 #searchInput { color: var(--ink); font-size: 13px; }
  /* Colour deliberately NOT set: the shipped base rule already says
     `color: var(--text-muted)`, and this lane used to override it to #a09a8c —
     2.37:1 on the chip's own ground, the least legible text in the app, and
     only on desktop, since no other lane lightened it. Inheriting the token
     puts it at 5.34:1 and means it follows the token from here on. */
  #app.v2 .search-hint { border: 1px solid var(--line); }

  /* Squared outline buttons. The design gives the header no filled control at
     all — Scan is the gold bar at the bottom edge, and nothing up here is
     allowed to compete with it. */
  #app.v2 .header-actions .btn,
  #app.v2 .header-actions .btn-primary,
  #app.v2 .header-actions .btn-ghost {
    background: none; border: 1px solid var(--ink); color: var(--ink);
    padding: 11px 15px; font-size: 13px; font-weight: 500;
  }
  #app.v2 .header-actions .btn:hover { background: var(--v2-hover); }
  #app.v2 .header-actions .btn-icon {
    border-color: var(--line); color: var(--text-secondary);
    width: 38px; height: 38px; padding: 0;
  }
  #app.v2 .header-actions .btn-icon:hover { border-color: var(--ink); color: var(--ink); }

  /* ---- NOTICE STRIP ----------------------------------------------------
     One tinted band under the header, flush to its edges — 7a runs the sticker
     notice and the intakes summary along a single strip rather than stacking
     two floating cards. */
  #app.v2 #stickerStockBar,
  #app.v2 #pendingBar {
    margin: 0; padding: 11px 24px;
    background: #f0eee6; border: none; border-bottom: 1px solid var(--line);
  }
  #app.v2 #pendingSummary, #app.v2 #pendingToggle { color: var(--accent); font-size: 12px; }

  /* ---- BOARD ------------------------------------------------------------
     Equal columns separated by a 1px rule drawn by the grid's own gap over an
     ink-line background — no borders on the columns themselves, so the seam
     between them is exactly one pixel however the columns resize. */
  /* .pane-orders in the selector is load-bearing: `#app.v2 #orderBoard` ties
     `#app:not(.pane-orders) #orderBoard { display: none }` on specificity and
     wins on source order, which left the board rendered UNDER the customers
     pane. Only style the board while the board is the pane. */
  #app.v2.pane-orders #orderBoard { display: flex; flex-direction: column; min-height: 0; }
  /* The shipped rail hides this heading to buy back vertical space; 7a spends
     it, and puts the queue filters on the same line. */
  #app.v2 .section-head { padding: 14px 24px 12px; margin: 0; }
  #app.v2 #orderBoard .section-head h2 {
    display: block; font-size: 19px; font-weight: 700; color: var(--ink);
    text-transform: none; letter-spacing: 0;
  }
  /* The shipped rail sets flex-end because it hid the heading and left the
     history link stranded at the left edge. With the heading back, the row is
     a title at one end and a link at the other again. */
  #app.v2 #orderBoard .section-head { justify-content: space-between; }
  #app.v2 .section-head .head-link { color: var(--accent); font-size: 13px; }
  #app.v2 .queue-grid {
    flex: 1; gap: 1px; background: var(--line); min-height: 0;
  }
  #app.v2 .queue-card {
    background: var(--paper); border: none; padding: 0;
    height: auto; min-height: 0;
  }
  #app.v2 .queue-top {
    padding: 13px 20px; margin: 0;
    background: var(--paper-3); border-bottom: 1px solid var(--line);
  }
  #app.v2 .queue-label {
    font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
    color: var(--text-secondary); font-weight: 400;
  }
  #app.v2 .count-badge {
    min-width: 22px; padding: 3px 8px; font-size: 11px; text-align: center;
    background: var(--ink); color: var(--paper);
  }
  #app.v2 .ready .count-badge { background: var(--success); color: var(--paper); }
  /* The Due⇄ROH switch keeps its behaviour and loses its pill — squared, ink
     hairline, the active half filled. */
  #app.v2 .sort-switch { border: 1px solid var(--ink); background: none; padding: 0; }
  #app.v2 .sort-opt {
    padding: 4px 9px; font-size: 9px; letter-spacing: 0.12em;
    text-transform: uppercase; color: var(--text-secondary); z-index: 1;
  }
  #app.v2 .sort-thumb { background: var(--ink) !important; border-radius: 0 !important; }
  #app.v2 .sort-opt.active { color: var(--paper); }
  #app.v2 #waitingOrders, #app.v2 #readyOrders { overflow-y: auto; min-height: 0; }

  /* ---- ORDER CARD -------------------------------------------------------
     A row on a rule, not a card on a surface. The shipped markup already has
     the right bones (.order-body / .order-side, name + meta + detail lines),
     so this is entirely a restyle — app.js is untouched. */
  #app.v2 .order-card {
    display: grid; grid-template-columns: 1fr 44px; gap: 12px;
    padding: 15px 20px; margin: 0;
    background: none; border: none; border-bottom: 1px solid var(--line-soft);
  }
  #app.v2 .order-card:hover { background: #eeebe3; }
  #app.v2 .order-card.ready { background: none; }
  #app.v2 .order-body { display: flex; flex-direction: column; gap: 7px; min-width: 0; }
  #app.v2 .order-name {
    font-size: 17px; font-weight: 500; color: var(--ink);
    display: flex; align-items: center; gap: 7px; flex-wrap: wrap; margin: 0;
  }
  /* Hollow is the biggest thing on the row — it is what the operator is about
     to dial into the machine. */
  #app.v2 .order-meta {
    display: flex; align-items: baseline; gap: 10px;
    font-size: 10px; letter-spacing: 0.05em; text-transform: uppercase;
    color: var(--text-secondary); margin: 0;
  }
  #app.v2 .order-hollow { font-size: 21px; font-weight: 700; color: var(--ink); font-family: 'Space Grotesk', sans-serif; }

  /* Every chip in this system: mono, 9–10px, wide tracking, square, 1px edge. */
  #app.v2 .order-code-chip, #app.v2 .skater-code-chip,
  #app.v2 .order-team-badge, #app.v2 .order-walkin-badge {
    font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.1em;
    padding: 2px 6px; text-transform: uppercase;
  }
  #app.v2 .order-code-chip, #app.v2 .skater-code-chip {
    background: var(--paper-3); border: 1px solid var(--line-chip); color: var(--text-secondary);
  }
  #app.v2 .order-team-badge { border: 1px solid var(--accent); color: var(--accent); background: none; }
  #app.v2 .order-walkin-badge { border: 1px dashed #a09a8c; color: var(--text-muted); background: none; }

  /* Action square: ink fill, teal on hover. The cancel is a lighter outline
     beneath it — same column, deliberately not the same weight. */
  #app.v2 .order-side { display: flex; flex-direction: column; gap: 7px; }
  #app.v2 .order-act {
    width: 44px; height: 44px; padding: 0;
    background: var(--ink); color: var(--paper); border: none; font-size: 19px;
  }
  #app.v2 .order-act:hover { background: var(--accent); }
  #app.v2 .order-act-ico { width: 19px; height: 19px; display: block; }
  #app.v2 .order-cancel {
    width: 44px; height: 36px; padding: 0;
    background: none; border: 1px solid var(--line-chip); color: var(--text-muted); font-size: 13px;
  }
  #app.v2 .order-cancel:hover { border-color: var(--danger); color: var(--danger); }

  /* ---- SCAN BAR ---------------------------------------------------------
     A full-width gold bar on the bottom content edge with a 2px ink top rule.
     The shipped slab is gated to `pointer: coarse`, so on a desktop review
     screen it is not rendered at all — v2 shows it inside the rail layout
     because in this design the bar IS the primary control, and a tablet
     screen reviewed without it is not the screen. */
  /* #scanSlab precedes #customersView in the DOM, so a static bar rendered
     between the two panes rather than under whichever one is up. Fixed to the
     content column's bottom edge instead — which is where 7a draws it, and
     what the shipped touch slab already does for the same reason. */
  /* The slab is `fixed`, so it is positioned against the VIEWPORT while the
     board it belongs to is positioned inside #app — and past ~1330px #app
     stops filling the window and centres itself (--app-max plus the rail). At
     1800px that put the rail at x=236 and the slab still at x=222: the gold bar
     ran 236px underneath the rail and past the board's right edge.
     So it has to retrace #app's own geometry, which is what the shipped touch
     slab does for the same reason. The gutter is however much the window
     exceeds the app's cap, halved; max() keeps it at 0 before the cap bites. */
  #app.v2 {
    --v2-gutter: max(0px, calc((100vw - var(--app-max)) / 2));
  }
  #app.v2 .scan-slab {
    display: block; position: fixed; bottom: 0;
    left: calc(var(--v2-gutter) + var(--rail-w));
    right: var(--v2-gutter);
    z-index: 50; padding: 0; background: none; grid-column: 2;
  }
  /* Clear the fixed bar so the last row of either pane stays reachable. */
  #app.v2 { padding-bottom: var(--v2-scan-h); }
  /* Same trap as .scan-slab-row below: the shipped `display: flex` is inside
     the `pointer: coarse` block, so on a desktop-width review the button was a
     plain inline-block and every flex property here was inert. State it. */
  #app.v2 .scan-slab-btn {
    display: flex; width: 100%; height: auto; padding: 22px;
    flex-direction: row; justify-content: center; align-items: center; gap: 18px;
    background: var(--gold); color: var(--ink);
    border: none; border-top: 2px solid var(--ink);
    font-family: 'Space Grotesk', sans-serif;
  }
  #app.v2 .scan-slab-btn:hover { background: var(--gold-hover); }
  #app.v2 .scan-slab-btn:active { transform: none; background: var(--gold-hover); }
  /* .scan-slab-row only becomes a flex container inside the shipped
     `pointer: coarse` block, which a desktop review never enters — so on the
     tablet-width screen it was a plain inline span and the block-level QR glyph
     broke out onto its own line at the left edge. v2 states it itself. */
  #app.v2 .scan-slab-row { display: flex; align-items: center; gap: 18px; }
  /* A QR glyph, not the camera lens: the bar's subject is the sticker on the
     boot, and the shipped lens says "take a photo". Both live in the markup;
     each theme shows its own. */
  #app.v2 .scan-slab-lens { display: none; }
  #app.v2 .scan-slab-qr { display: block; width: 38px; height: 38px; flex: none; }
  #app.v2 .scan-slab-title {
    font-size: 24px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase;
  }
  #app.v2 .scan-slab-hint {
    font-size: 11px; letter-spacing: 0.12em; color: var(--gold-ink); opacity: 1;
    text-transform: uppercase;
  }
  /* The header's Scan chip is hidden wherever the slab is up — one control per
     destination. The shipped rule lives in a `pointer: coarse` block that a
     desktop review never enters, so v2 restates it. */
  #app.v2 #scanBtn { display: none; }

  /* ---- RAIL FOOT --------------------------------------------------------
     The day's numbers move OFF the board and into the rail (7a). The strip
     above the board is the shipped presentation of the same computeStats call;
     exactly one of the two is ever visible. */
  #app.v2 .stats-bar { display: none; }
  #app.v2 .rail-stats { display: block; margin-top: auto; }
  /* margin-top:auto on BOTH this and #tabSettings would split the slack between
     them and float the stats block in the middle of the rail. The foot takes
     the slack; Settings just sits under it. */
  #app.v2 #tabSettings { margin-top: 0; }

  #app.v2 .rs-block {
    display: flex; flex-direction: column; gap: 13px;
    padding: 18px 20px; border-top: 1px solid var(--line);
  }
  /* The period control keeps the shipped pattern — a transparent <select> over
     a styled label — so changing period still runs setStatsPeriod and nothing
     new has to know about periods. */
  #app.v2 .rs-period { position: relative; display: flex; align-items: center; gap: 6px; cursor: pointer; }
  #app.v2 .rs-period select {
    position: absolute; inset: 0; width: 100%; height: 100%;
    opacity: 0; cursor: pointer; font: inherit;
  }
  #app.v2 .rs-period-label {
    font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.14em;
    text-transform: uppercase; color: var(--text-muted);
  }
  #app.v2 .rs-period-caret { font-size: 9px; color: var(--text-muted); }

  #app.v2 .rs-stat { display: flex; flex-direction: column; gap: 2px; }
  #app.v2 .rs-value { font-size: 25px; font-weight: 700; letter-spacing: -0.02em; color: var(--ink); }
  #app.v2 .rs-label {
    font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.12em;
    text-transform: uppercase; color: var(--text-muted);
  }

  #app.v2 .rs-methods {
    display: flex; flex-direction: column; gap: 5px;
    padding-top: 11px; border-top: 1px solid var(--line);
  }
  #app.v2 .rs-row { display: flex; align-items: baseline; gap: 8px; }
  #app.v2 .rs-row-k {
    font-family: 'DM Mono', monospace; font-size: 11px; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--text-secondary);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* A dotted leader, so a short method name and a long one still land their
     amounts on the same right edge. */
  #app.v2 .rs-lead { flex: 1; min-width: 8px; border-bottom: 1px dotted var(--line-chip); }
  #app.v2 .rs-row-v { font-family: 'DM Mono', monospace; font-size: 11px; color: var(--ink); white-space: nowrap; }

  #app.v2 .rs-owed {
    display: flex; align-items: center; gap: 9px;
    padding: 16px 20px; border-top: 1px solid var(--line);
    color: var(--danger);
  }
  #app.v2 .rs-owed .rs-label { color: var(--danger); }
  #app.v2 .rs-owed-amt {
    padding: 2px 6px; background: var(--danger); color: var(--paper);
    font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.06em;
  }
}

/* The hardware shortcut belongs beside Scan on pointer-driven desktops. The
   shared markup places it before the QR glyph; touch layouts keep it hidden. */
@media (hover: hover) and (min-width: 1000px) {
  #app.v2 .scan-slab-shortcut {
    display: inline-block; padding: 3px 7px;
    border: 1px solid var(--ink);
    font-family: 'DM Mono', monospace; font-size: 10px;
    font-weight: 600; line-height: 1; letter-spacing: 0.08em;
  }
}

/* ============================================================================
   CUSTOMERS — screen 5a
   The shell (rail, header, gold scan bar) is already the one 7a established
   above; only the pane's own content is new here.

   ⚠️ 5a's own shell differs: it puts SCAN in the rail as an ink block under the
   brand, and its rail foot has no by-method or outstanding. 7a is two turns
   newer and moved Scan to the gold bottom bar, so the shell built for 7a wins
   and this screen inherits it. Do not "restore" 5a's rail scan block.

   ⚠️ The handoff README's screen table calls `5d` the Customers screen. It is
   not — 5d is an orders board with a gold bottom bar. `5a` is Customers.
   ========================================================================= */

/* v2-only markup in the shared toolbar. */
.pane-title, .pane-count, .pane-link { display: none; }
.qs-ico { width: 15px; height: 15px; display: block; }

@media (pointer: coarse) and (hover: none) and (orientation: landscape),
       (hover: hover) and (min-width: 1000px) {

  /* The blanket radius-0 rule squares everything, but the avatar is the one
     round object the design keeps — a circle of initials, in both the list and
     the detail header. */
  #app.v2 .avatar, #app.v2 .avatar img, #app.v2 .detail-avatar {
    border-radius: 50% !important;
  }

  /* ---- TITLE ROW ---------------------------------------------------------
     The pane names itself, states its count, and parks the sort control at the
     far end as ONE segmented group rather than three loose buttons. */
  /* flex-direction is load-bearing: the shipped .toolbar is a COLUMN with
     align-items:stretch, so setting only align-items:center stacked the title,
     the count and the sort group and centred each one. */
  #app.v2 #customersView .toolbar {
    display: flex; flex-direction: row; align-items: center; gap: 14px;
    flex-wrap: nowrap; margin: 0; padding: 14px 24px;
    border-bottom: 1px solid var(--line); background: none;
  }
  #app.v2 .pane-link {
    display: block; background: none; border: none; padding: 0;
    color: var(--accent); font-size: 13px; cursor: pointer; white-space: nowrap;
  }
  #app.v2 .pane-link:hover { text-decoration: underline; }
  #app.v2 .pane-title { display: block; font-size: 19px; font-weight: 700; color: var(--ink); }
  #app.v2 .pane-count {
    display: block; font-family: 'DM Mono', monospace; font-size: 11px;
    letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted);
  }
  /* A single bordered group: one ink hairline around the set, 1px rules
     between, the active segment filled. */
  #app.v2 #customersView .toolbar-filters {
    margin-left: auto; display: flex; gap: 0; flex-wrap: nowrap;
    border: 1px solid var(--ink);
  }
  #app.v2 #customersView .toolbar-filters .btn {
    background: none; border: none; border-right: 1px solid var(--ink);
    padding: 8px 13px; color: var(--text-secondary);
    font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.12em;
    text-transform: uppercase;
  }
  #app.v2 #customersView .toolbar-filters .btn:last-child { border-right: none; }
  #app.v2 #customersView .toolbar-filters .btn:hover { background: var(--v2-hover); color: var(--ink); }
  #app.v2 #customersView .toolbar-filters .btn.btn-primary { background: var(--ink); color: var(--paper); }
  /* The shipped count line is redundant once the title row carries it. */
  #app.v2 #customersView .list-count { display: none; }

  /* ---- THREE COLUMNS -----------------------------------------------------
     List, a 26px letter rail, and a fixed 386px record. Panels meet on 1px
     rules with no gutter — the gap in the shipped grid is what made these read
     as three floating cards instead of one surface. */
  #app.v2 #custLayout,
  #app.v2 #custLayout.rail-off { grid-template-columns: minmax(0,1fr) 26px 386px; gap: 0; }
  #app.v2 #custLayout.rail-off .az-rail { display: none; }
  #app.v2 #custLayout.rail-off { grid-template-columns: minmax(0,1fr) 386px; }
  /* A definite height so the three columns scroll INSIDE themselves and the
     page itself never scrolls — the same move the board's lanes make. The
     subtraction covers the header row, the title row and the fixed scan bar;
     viewport units because the exact top of the layout moves with them. */
  /* Takes whatever the title row leaves, so the three columns scroll INSIDE
     themselves and the page never does. flex:1 rather than a calc — the header
     and the title row size themselves, and no number here has to track them. */
  #app.v2 .cust-layout { align-items: stretch; flex: 1; min-height: 0; height: auto; }
  #app.v2 .customer-grid {
    background: var(--paper); padding: 0; gap: 0;
    max-height: none; height: 100%; overflow-y: auto; min-height: 0;
  }
  #app.v2 .az-rail {
    position: static; background: var(--paper-2);
    border-left: 1px solid var(--line-soft); border-right: 1px solid var(--line);
    padding: 8px 0; gap: 0; align-self: stretch;
  }
  #app.v2 .az-rail button {
    font-size: 9px; line-height: 1.55; letter-spacing: 0.04em; color: var(--text-secondary);
  }
  #app.v2 .az-rail button:hover:not(:disabled) { color: var(--ink); }
  #app.v2 .detail-col {
    position: static; background: var(--paper-2);
    height: 100%; overflow-y: auto; min-height: 0;
  }

  /* ---- LIST ROW ---------------------------------------------------------- */
  #app.v2 .customer-card {
    grid-template-columns: 38px minmax(0,1fr) auto 36px; gap: 14px;
    padding: 13px 22px; margin: 0;
    background: none; border: none; border-bottom: 1px solid var(--line-soft);
    border-left: 3px solid transparent;
    backdrop-filter: none; transform: none;
  }
  #app.v2 .customer-card:hover { background: #eeebe3; transform: none; }
  /* The selected row is marked by the ink bar on its edge, not by a border
     that has to fight the row rules for attention. */
  #app.v2 .customer-card.active { background: var(--paper-2); border-left-color: var(--ink); }
  #app.v2 .customer-card.kbd-active { border-left-color: var(--accent); box-shadow: none !important; }
  #app.v2 .avatar {
    width: 38px; height: 38px; background: var(--line-soft);
    border: 1px solid var(--line-chip); color: var(--text-secondary);
    font-size: 13px; font-weight: 700;
  }
  #app.v2 .customer-name { font-size: 15px; font-weight: 500; color: var(--ink); }
  #app.v2 .customer-meta {
    font-family: 'DM Mono', monospace; font-size: 11px; color: var(--text-muted);
    text-transform: none; letter-spacing: 0;
  }
  #app.v2 .customer-right { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
  #app.v2 .visit-count { font-size: 18px; font-weight: 700; color: var(--ink); }
  #app.v2 .visit-label {
    font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--text-muted);
  }
  #app.v2 .balance-badge {
    font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.08em;
    text-transform: uppercase; padding: 2px 7px; background: none;
    border: 1px solid var(--line-chip); color: var(--text-secondary);
  }
  #app.v2 .balance-owed { border-color: var(--danger); color: var(--danger); }
  /* An outlined square that inverts on hover — the row's one action, and the
     only control on it that isn't the row itself. */
  #app.v2 .quick-sharpen-btn {
    width: 36px; height: 36px; padding: 0;
    display: flex; align-items: center; justify-content: center;
    background: none; border: 1px solid var(--ink); color: var(--ink); font-size: 0;
  }
  #app.v2 .quick-sharpen-btn:hover { background: var(--ink); color: var(--paper); }

  /* Letter headers are a band across the list, not a line of text floating on
     it — they still stick, so the letter stays legible while you scroll. */
  #app.v2 .letter-header {
    background: var(--paper-3); border-bottom: 1px solid var(--line);
    padding: 9px 22px; font-size: 10px; font-weight: 400;
    letter-spacing: 0.18em; color: #6b675e;
  }
  #app.v2 .list-section-title {
    background: var(--paper-3); border-bottom: 1px solid var(--line);
    padding: 9px 22px; font-size: 10px; letter-spacing: 0.18em; color: #6b675e;
  }
  #app.v2 .show-more-btn {
    margin: 0; border: none; border-bottom: 1px solid var(--line-soft);
    background: none; color: var(--accent);
    font-family: 'DM Mono', monospace; font-size: 11px; letter-spacing: 0.1em;
    text-transform: uppercase; padding: 13px 22px;
  }

  /* ---- DETAIL PANEL ------------------------------------------------------
     A 2px ink rule under the header is the heaviest line in the pane — the
     same weight the rail's brand block uses, marking "this is a record, not a
     row". */
  #app.v2 .detail-col .detail-card,
  #app.v2 .detail-col > div { background: none; border: none; }
  #app.v2 .detail-empty {
    background: none; border: none; color: var(--text-muted);
    padding: 56px 24px; font-size: 14px;
  }
  #app.v2 .detail-back {
    border: 1px solid var(--ink); color: var(--ink); background: none;
    font-family: 'DM Mono', monospace; font-size: 11px; letter-spacing: 0.1em;
    text-transform: uppercase;
  }
}

/* Tablet landscape keeps the phone-land scan-first placement but gives the
   control a roomier right rail. This follows the rail shell above so it wins
   over the default bottom-slab layout. */
@media (pointer: coarse) and (hover: none) and (orientation: landscape) and (min-width: 641px) {
  #app.v2 {
    --v2-tablet-scan-rail-w: 124px;
    --v2-scan-h: 0px;
    padding: 0 var(--v2-tablet-scan-rail-w) 0 0;
  }
  #app.v2 .scan-slab {
    top: 0; bottom: 0; left: auto; right: 0; width: var(--v2-tablet-scan-rail-w);
    padding: 0; background: none; border: none;
  }
  #app.v2 .scan-slab-btn {
    width: 100%; height: 100%; padding: 14px 10px; border: none; border-left: 2px solid var(--ink);
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;
    background: var(--gold); color: var(--ink);
  }
  #app.v2 .scan-slab-row { display: contents; }
  #app.v2 .scan-slab-qr { display: block; width: 44px; height: 44px; }
  #app.v2 .scan-slab-title { font-size: 26px; letter-spacing: 0.02em; }
  #app.v2 .scan-slab-hint {
    display: block; padding: 0 10px; text-align: center;
    font-family: 'DM Mono', monospace; font-size: 10px; line-height: 1.4;
  }
}

/* ---- CUSTOMER DETAIL PANEL (5a, right column) ---------------------------- */
@media (pointer: coarse) and (hover: none) and (orientation: landscape),
       (hover: hover) and (min-width: 1000px) {

  /* A panel, not a floating card: it is one of three columns that meet on 1px
     rules, so the lift and the slide-in both had to go. */
  #app.v2 .detail-panel.inline {
    background: none; border: none; box-shadow: none !important;
    animation: none; margin: 0; padding: 0;
  }
  /* 2px ink under the identity block — the same weight the rail's brand block
     uses, marking "this is a record". */
  #app.v2 .detail-header {
    margin: 0; padding: 20px; gap: 14px;
    border-bottom: 2px solid var(--ink); background: var(--paper-2);
  }
  #app.v2 .detail-identity { gap: 14px; }
  /* The avatar is inline-styled in app.js, so its size and colours come from
     there (and from the tokens above). Only the ring is worth restating. */
  #app.v2 .detail-identity > div:first-child {
    border-color: var(--line-chip) !important;
    color: var(--text-secondary) !important;
  }
  #app.v2 .detail-name { font-size: 22px; font-weight: 700; letter-spacing: -0.01em; color: var(--ink); }
  #app.v2 .detail-contact { font-size: 11px; color: var(--text-secondary); }
  /* The record's primary action is the one filled control in the pane. */
  #app.v2 .detail-queue-btn {
    background: var(--ink); border: 1px solid var(--ink); color: var(--paper);
    font-weight: 500;
  }
  #app.v2 .detail-queue-btn:hover { background: var(--accent); border-color: var(--accent); }
  #app.v2 .detail-actions .btn {
    background: none; border: 1px solid var(--line-chip); color: var(--text-secondary);
  }
  #app.v2 .detail-actions .btn:hover { border-color: var(--ink); color: var(--ink); }
  /* Everything below the header is a stack of sections on paper, separated by
     rules rather than by cards on a surface. */
  #app.v2 .detail-panel.inline > *:not(.detail-header):not(.detail-back) {
    padding-left: 20px; padding-right: 20px;
  }
}

/* ---- DETAIL PANEL SECTIONS ------------------------------------------------
   These carry LITERAL rgba(77,184,212,…) — a hardcoded accent tint rather than
   a token — so remapping --accent above cannot reach them and they stayed dark
   blue on a paper panel. This is the class of thing that made a global :root
   swap untenable; here it is a handful of rules on one screen. */
@media (pointer: coarse) and (hover: none) and (orientation: landscape),
       (hover: hover) and (min-width: 1000px) {

  /* (The account holder's card is handled at EVERY width now — see the
     primary-card block near the top of this file.) */
  #app.v2 .primary-card .pc-name { font-size: 14px; font-weight: 600; color: var(--ink); }
  /* PRIMARY was a tinted accent pill — the last of the dark theme's rgba in
     this panel. A mono chip like every other chip in the system. */
  #app.v2 .badge-primary {
    background: none !important; border: 1px solid var(--line-chip);
    color: var(--text-secondary) !important;
    font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.1em;
    text-transform: uppercase; padding: 2px 6px; font-weight: 400;
  }
  /* The extra-skater blocks still have a paper-3 ground, which is exactly the
     code chip's own fill — it vanished on them. Lift those to the input white.
     The primary card no longer needs it now the fill is gone. */
  #app.v2 .skater-compact .skater-code-chip { background: #fbfaf7; }

  #app.v2 .skater-compact { background: var(--paper); border: 1px solid var(--line); }
  #app.v2 .skater-head:hover { background: var(--v2-hover); }
  #app.v2 .skater-head .sh-name { color: var(--ink); }

  /* The per-skater quick-sharpen, same square as the list row's. */
  #app.v2 .skater-zap {
    width: 30px; height: 30px;
    border: 1px solid var(--ink); background: none; color: var(--ink);
  }
  #app.v2 .skater-zap:hover { background: var(--ink); color: var(--paper); }

  #app.v2 .pref-label {
    font-size: 10px; letter-spacing: 0.12em; color: var(--text-muted);
  }
  #app.v2 .pref-value { color: var(--ink); }
}

/* ============================================================================
 WHEEL-SIDE MODE — screen 7c, 844x390

 ⚠️ THE GEOMETRY IN THIS SECTION AND IN PHONE LANDSCAPE BELOW IS UNSCOPED —
 `#app.wheel-on`, not `#app.v2.wheel-on` — so BOTH themes read it. app.js sets
 .wheel-on / .phone-land in either theme, but only v2 ever styled them, so the
 dark theme had no landscape layout at all: at 844x390 it showed the portrait
 phone layout lying down, header and all.

 Splitting it HERE rather than moving it to index.html is deliberate. These
 rules override earlier v2 rules of equal specificity — .rail-mark-ink is 34px
 at line 84 and 26px here — and they win on SOURCE ORDER. Moved to index.html
 they would load before v2.css, lose those ties and silently take the wrong
 values. Measured when I tried exactly that: 12 regressions in phone-land, 5 in
 wheel mode. v2.css is linked unconditionally, so unscoped rules in it already
 reach dark; the position in the cascade is what matters, not the file.

 What stays `.v2` is the paint. Each split rule is a pair: shared geometry
 first, the v2 colour immediately after it.

 A MODE, not a reflow. Rotating the phone at the machine means ONE job fills
 the screen; the portrait list does not turn on its side.

 THE GATE is the interesting part, and it is deliberately NOT a media query.
 The shipped rail block is `(pointer: coarse) and (hover: none) and
 (orientation: landscape)`, which a landscape TABLET also matches — orientation
 alone cannot tell a phone at the wheel from a tablet on the counter.
 `max-height: 500px` does: a phone in landscape is ~390-430px tall, a tablet
 ~800px. Height, not width, because width is the axis the two share.

 That test lives in syncWheelMode() in app.js and arrives here as `.wheel-on`.
 Two reasons. It makes the trigger INSPECTABLE — you can see in the DOM why the
 phone is in this mode. And `pointer: coarse` cannot be emulated at 844px wide
 in a desktop browser, so a pure media query would be unreviewable anywhere but
 real hardware; `?wheel=1` forces the class instead.
 ========================================================================= */
.wheel-mode { display: none; }


/* Everything else leaves EXCEPT the rail. One job owns the screen, but a screen with no way off it is a dead end — Customers, Teams and Reports were unreachable here without rotating the phone back. The rail costs 62px of 844 and the job pane still runs 520px wide, which the 56px hollow and 31px name sit in comfortably. Hiding the rest here rather than unwinding it in JS keeps rotation free: turn the phone back and the board is exactly as it was, with no state to restore. */ #app.wheel-on > *, #app.wheel-on .scan-slab { display: none !important; }
#app.wheel-on > .view-tabs { display: flex !important; }
#app.wheel-on { display: block; padding: 0; height: 100vh; height: 100dvh; overflow: hidden; }
/* ⚠️ The width tokens are UNSCOPED. Layout depends on them, and a custom property
   that layout depends on cannot be theme-scoped: defined only under `.v2` they
   resolve to nothing in dark, every `width: var(--v2-rail-w)` becomes invalid and
   is dropped, and the rail and slab collapse to content width without erroring.
   Measured while they were still scoped: rail 52px instead of 62, slab 210 not 124. */
#app.wheel-on { --v2-rail-w: 62px; }
#app.v2.wheel-on { background: var(--paper); }
#app.wheel-on .wheel-mode { display: flex !important; flex-direction: column; height: 100vh; height: 100dvh; margin-left: var(--v2-rail-w); }
#app.v2.wheel-on .wheel-mode { background: var(--paper); }

#app.wheel-on .wm-body { flex: 1; display: flex; min-height: 0; }
#app.wheel-on .wm-spacer { flex: 1; }

/* ---- THE JOB ---------------------------------------------------------- */ #app.wheel-on .wm-main { flex: 1; display: flex; flex-direction: column; gap: 10px; padding: 16px 20px; min-width: 0; }
#app.wheel-on .wm-head { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
#app.v2.wheel-on .wm-label {
  font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--text-muted);
}
#app.wheel-on .wm-code { padding: 2px 6px; }
#app.v2.wheel-on .wm-code { font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.1em; background: var(--ink); color: var(--paper); }
#app.wheel-on .wm-team { padding: 2px 6px; }
#app.v2.wheel-on .wm-team { font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase; border: 1px solid var(--accent); color: var(--accent); }
#app.v2.wheel-on .wm-name {
  font-size: 31px; font-weight: 700; letter-spacing: -0.02em; line-height: 1;
  color: var(--ink);
}
/* The hollow is the number being dialled into the machine, so it is the largest thing on any screen in this system. */ #app.wheel-on .wm-hollow-row { display: flex; align-items: baseline; gap: 13px; min-width: 0; }
#app.wheel-on .wm-hollow { flex: none; }
#app.v2.wheel-on .wm-hollow { font-size: 56px; font-weight: 700; letter-spacing: -0.03em; line-height: 0.85; color: var(--ink); }
#app.wheel-on .wm-hollow-side { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
#app.wheel-on .wm-pair { display: flex; align-items: center; gap: 7px; }
#app.v2.wheel-on .wm-pair-name { font-size: 14px; font-weight: 700; color: var(--ink); }
#app.v2.wheel-on .wm-spec, #app.v2.wheel-on .wm-for {
  font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.06em;
  text-transform: uppercase;
}
#app.v2.wheel-on .wm-spec { color: var(--text-secondary); }
#app.v2.wheel-on .wm-for { color: var(--text-muted); }

/* What the pickup will charge for, on an ink-edged block so it reads as attached to the job rather than as another chip row. */ #app.wheel-on .wm-extras { display: flex; flex-direction: column; gap: 3px; padding: 8px 10px; }
/* What the pickup will charge for, on an ink-edged block so it reads as attached to the job rather than as another chip row. */ #app.v2.wheel-on .wm-extras { background: var(--paper-2); border-left: 2px solid var(--ink); }
#app.wheel-on .wm-extra { display: flex; align-items: baseline; gap: 8px; }
#app.v2.wheel-on .wm-extra-kind {
  font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--text-muted);
}
#app.v2.wheel-on .wm-extra-name { font-size: 12px; color: var(--ink); }
#app.v2.wheel-on .wm-extra-price { font-family: 'DM Mono', monospace; font-size: 11px; color: var(--ink); }

#app.wheel-on .wm-chips { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
#app.wheel-on .wm-chip { padding: 3px 7px; }
#app.v2.wheel-on .wm-chip { font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; border: 1px solid var(--line-chip); color: var(--text-secondary); }
#app.v2.wheel-on .wm-chip-due { background: var(--danger); border-color: var(--danger); color: var(--paper); }
#app.v2.wheel-on .wm-chip-accent { border-color: var(--accent); color: var(--accent); }

/* ---- NEXT UP ----------------------------------------------------------- */ #app.wheel-on .wm-side { width: 262px; flex: none; display: flex; flex-direction: column; }
/* ---- NEXT UP ----------------------------------------------------------- */ #app.v2.wheel-on .wm-side { background: var(--paper-2); border-left: 1px solid var(--ink); }
#app.wheel-on .wm-next-head { padding: 8px 14px; flex: none; }
#app.v2.wheel-on .wm-next-head { background: var(--paper-3); border-bottom: 1px solid var(--line); font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--text-secondary); }
/* The rest of the rack. Scrolls inside itself so the header and the back control stay put however long the queue is. */ #app.wheel-on .wm-next-list { flex: 1; min-height: 0; overflow-y: auto; }

/* The team filter, moved in here by placeWheelFilter. Filtering to one team is how a rack gets batched, so it belongs at the machine — and it was silently in force here with no way to see or clear it. */ #app.wheel-on .wm-side .queue-filter-bar { display: flex !important; flex: none; gap: 5px; margin: 0; padding: 7px 10px; flex-wrap: nowrap; overflow-x: auto; }
/* The team filter, moved in here by placeWheelFilter. Filtering to one team is how a rack gets batched, so it belongs at the machine — and it was silently in force here with no way to see or clear it. */ #app.v2.wheel-on .wm-side .queue-filter-bar { scrollbar-width: none; background: var(--paper-3); border-bottom: 1px solid var(--ink); }
#app.wheel-on .wm-side .queue-filter-bar::-webkit-scrollbar { display: none; }
#app.wheel-on .qf-chip { flex: none; padding: 3px 8px; }
#app.v2.wheel-on .qf-chip { font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; border-radius: 0; background: none; border: 1px solid var(--line-chip); color: var(--text-secondary); white-space: nowrap; }
#app.v2.wheel-on .qf-chip.active {
  background: var(--ink); border-color: var(--ink); color: var(--paper); font-weight: 400;
}
#app.wheel-on .wm-next-row { display: flex; flex-direction: column; gap: 3px; padding: 10px 14px; }
#app.v2.wheel-on .wm-next-row { border-bottom: 1px solid var(--line-soft); }
#app.wheel-on .wm-next-top { display: flex; align-items: baseline; gap: 8px; }
#app.wheel-on .wm-next-name { flex: 1; min-width: 0; overflow: hidden; }
#app.v2.wheel-on .wm-next-name { font-size: 14px; font-weight: 500; color: var(--ink); text-overflow: ellipsis; white-space: nowrap; }
#app.v2.wheel-on .wm-next-hollow { font-size: 16px; font-weight: 700; color: var(--ink); }
#app.v2.wheel-on .wm-next-short {
  font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--text-muted);
}
#app.wheel-on .wm-next-empty { padding: 14px; }
#app.v2.wheel-on .wm-next-empty { font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
/* The slab is the way BACK, not a scan. Scanning is the counter's mode: at the wheel the operator is working a batch they already sorted, and stopping to identify a pair means stepping back to the queue anyway. So this is navigation, and it is deliberately quieter than the ink action bar below — paper-3 with an ink edge, not gold. Gold would read as the primary thing to press on a screen whose primary thing is "mark ready". */ #app.wheel-on .wm-back { display: flex; align-items: center; justify-content: center; gap: 9px; padding: 13px; flex: none; }
/* The slab is the way BACK, not a scan. Scanning is the counter's mode: at the wheel the operator is working a batch they already sorted, and stopping to identify a pair means stepping back to the queue anyway. So this is navigation, and it is deliberately quieter than the ink action bar below — paper-3 with an ink edge, not gold. Gold would read as the primary thing to press on a screen whose primary thing is "mark ready". */ #app.v2.wheel-on .wm-back { background: var(--paper-3); color: var(--ink); border: none; border-top: 1px solid var(--ink); cursor: pointer; font-family: 'DM Mono', monospace; font-size: 11px; font-weight: 500; letter-spacing: 0.14em; text-transform: uppercase; }
#app.v2.wheel-on .wm-back:active { background: var(--v2-hover); }
#app.wheel-on .wm-back-ico { width: 18px; height: 18px; display: block; flex: none; }

/* ---- THE ONE ACTION ---------------------------------------------------- */ #app.wheel-on .wm-act { display: flex; align-items: center; justify-content: center; gap: 12px; width: 100%; padding: 14px; flex: none; }
/* ---- THE ONE ACTION ---------------------------------------------------- */ #app.v2.wheel-on .wm-act { background: var(--ink); color: var(--paper); border: none; cursor: pointer; font-family: 'Space Grotesk', sans-serif; font-size: 17px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; }
#app.v2.wheel-on .wm-act:active { background: var(--success); }
#app.wheel-on .wm-act-ico { width: 21px; height: 21px; display: block; }

/* ---- EMPTY RACK -------------------------------------------------------- */ #app.wheel-on .wm-empty { justify-content: center; gap: 6px; }
#app.v2.wheel-on .wm-empty-title { font-size: 28px; font-weight: 700; letter-spacing: -0.02em; color: var(--ink); }
#app.v2.wheel-on .wm-empty-sub { font-size: 13px; color: var(--text-secondary); }

/* ============================================================================
   PHONE PORTRAIT — screen 6c, 390x844
   Header, WAITING/READY switch, one lane of rows, gold SCAN, bottom tab bar.

   Most of this screen already existed. The segmented switch is the shipped
   `.lane-seg` (one lane at a time, both counts carried) and the scan bar is the
   shipped `.scan-slab`; 6c re-dresses them and moves the nav to the bottom.

   ⚠️ DEVIATION, deliberate: 6c's header holds ONLY the mark, the pane name and
   the day. It has no search and no + Order. Both are kept here. The shipped app
   moved search into the header precisely so a walk-up with no tag is answerable
   from any pane, and this design's own tablet scan bar reads "NO TAG? SEARCH,
   OR USE + ORDER" — so deleting both on the phone would remove the fallback the
   system tells the operator to use. They keep their own row under the header.
   ========================================================================= */
.brand-mark, .brand-pane, .brand-day, .brand-wordmark,
.brand-mark-ink, .rail-mark-ink, .tab-ico-v2 { display: none; }

@media (max-width: 640px) {

  #app.v2 { padding: 0; background: var(--paper); }

  /* 6c switches PANES from the bottom bar: the board is one destination of
     four, not a permanent strip above whatever else is open. The shipped
     portrait stacks the board and the current view, which is why Customers'
     toolbar was visible under the queue. This is the same pane model the rail
     block already uses on tablet — reused here, not reinvented. */
  #app.v2:not(.pane-orders) #orderBoard { display: none; }
  #app.v2.pane-orders #customersView, #app.v2.pane-orders #teamsView,
  #app.v2.pane-orders #reportsView, #app.v2.pane-orders #adminView { display: none !important; }
  #app.v2.pane-orders .view-tab.active { background: none; color: var(--text-secondary); }
  #app.v2 #tabOrders { display: flex; }   /* the board is a destination here too */

  /* The header line carries the day, so the strip above the board would say it
     a second time — and cost a row the phone cannot spare. */
  #app.v2 .stats-bar { display: none; }

  /* ---- APP HEADER -------------------------------------------------------- */
  #app.v2 > header.header {
    margin: 0; padding: 14px 16px; gap: 10px;
    justify-content: flex-start; align-content: flex-start;
    border-bottom: 1px solid var(--ink); background: var(--paper);
  }
  #app.v2 .brand {
    display: flex; align-items: center; justify-content: flex-start;
    align-self: flex-start; gap: 10px; flex: 1 1 100%; min-width: 0; margin-right: auto;
  }
  /* The phone header carries the actual lockup, anchored at the left — the pane
     is already named by the selected bottom tab, so repeating it here wastes
     the brand's only home on this compact layout. */
  #app.v2 .brand-mark-ink, #app.v2 .brand-wordmark { display: block; }
  #app.v2 .brand-pane { display: none; }
  #app.v2 .brand-day {
    display: block; margin-left: auto;
    font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.1em;
    text-transform: uppercase; color: var(--text-muted); white-space: nowrap;
  }
  /* The kept fallbacks. Squared to match, and quieter than the gold bar so the
     scan-first reading of the screen survives. */
  #app.v2 .header-search { background: #fbfaf7; border: 1px solid var(--ink); margin: 0; }
  #app.v2 .header-actions .btn {
    background: none; border: 1px solid var(--line-chip); color: var(--text-secondary);
  }

  /* ---- WAITING / READY --------------------------------------------------- */
  #app.v2 .lane-seg {
    display: flex; gap: 0; margin: 0; padding: 0;
    background: var(--paper-3); border: none; border-bottom: 1px solid var(--ink);
  }
  #app.v2 .lane-seg-opt {
    flex: 1; height: 48px; min-height: 48px; gap: 8px;
    background: none; color: var(--text-secondary);
    font-family: 'DM Mono', monospace; font-size: 11px; font-weight: 400;
    letter-spacing: 0.16em;
  }
  #app.v2 .lane-seg-opt + .lane-seg-opt { border-left: 1px solid var(--line); }
  #app.v2 .lane-seg-opt:hover { background: #e0dcd2; }
  #app.v2 .lane-seg-opt.active { background: var(--ink); color: var(--paper); }
  #app.v2 .lane-seg-count {
    min-width: 0; height: auto; padding: 1px 6px;
    font-family: 'DM Mono', monospace; font-size: 11px; font-weight: 400;
    background: none; border: 1px solid var(--line-chip); color: var(--text-secondary);
  }
  /* The active lane's count is filled, in that lane's own colour — gold for
     Waiting, green for Ready, the pair the board already uses. */
  #app.v2 .lane-seg-opt.waiting.active .lane-seg-count {
    background: var(--gold); border-color: var(--gold); color: var(--ink);
  }
  #app.v2 .lane-seg-opt.ready .lane-seg-count { border-color: var(--success); color: var(--success); }
  #app.v2 .lane-seg-opt.ready.active .lane-seg-count {
    background: var(--success); border-color: var(--success); color: var(--paper);
  }

  /* ---- LIST -------------------------------------------------------------- */
  #app.v2 #orderBoard { padding: 0; }
  #app.v2 #orderBoard .section-head { display: none; }   /* the header already names the pane */
  #app.v2 .queue-grid { gap: 0; background: none; }
  /* .has-items paints a literal rgba() lane tint — right in the dark theme,
     wrong here, where the lane is already named by the filled segment above. */
  #app.v2 .queue-card,
  #app.v2 .queue-card.waiting.has-items,
  #app.v2 .queue-card.ready.has-items {
    background: none; border: none; padding: 0;
  }
  /* The Due/ROH switch is the only thing left in the lane header (the shipped
     phone block already hides the label and count). 6c has no room to draw it,
     but it is how a rack gets batched, so it stays — squared and quiet. */
  #app.v2 .queue-top { padding: 3px 16px; margin: 0; justify-content: flex-end; }
  /* Waiting shares one control row: team filters consume the flexible left side,
     while Due/ROH stays pinned at the right. Chips scroll rather than wrapping
     into the vertical space this arrangement is meant to recover. */
  /* minmax(0, 1fr) is important here: a plain 1fr grid track has an automatic
     minimum equal to its longest chip, which let extra teams widen the page. */
  #app.v2 .queue-grid { grid-template-columns: minmax(0, 1fr); min-width: 0; }
  #app.v2 #waitingCard { min-width: 0; }
  #app.v2 #waitingCard .queue-top { gap: 8px; min-width: 0; overflow: hidden; }
  #app.v2 #waitingCard .queue-filter-bar {
    display: flex !important; flex: 1 1 0; width: 0; min-width: 0; gap: 5px;
    margin: 0; padding: 0; flex-wrap: nowrap; overflow-x: auto;
    overscroll-behavior-x: contain; scrollbar-width: none;
    -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 24px), transparent 100%);
    mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 24px), transparent 100%);
  }
  #app.v2 #waitingCard .queue-filter-bar::-webkit-scrollbar { display: none; }
  #app.v2 #waitingCard .qf-chip { flex: none; padding: 3px 8px; }
  #app.v2 #waitingCard .queue-top > div:last-child { flex: 0 0 auto; position: relative; z-index: 1; }
  #app.v2 #readyCard .queue-top { display: none; }
  #app.v2 .sort-switch { border: 1px solid var(--ink); background: none; padding: 0; }
  #app.v2 .sort-opt {
    padding: 4px 9px; font-family: 'DM Mono', monospace; font-size: 9px;
    letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-secondary); z-index: 1;
  }
  #app.v2 .sort-thumb { background: var(--ink) !important; }
  #app.v2 .sort-opt.active { color: var(--paper); }
  #app.v2 .order-card {
    grid-template-columns: minmax(0, 1fr) 52px; gap: 12px; align-items: center;
    padding: 14px 16px; border-bottom: 1px solid var(--line-soft);
  }
  #app.v2 .order-card { background: none; border-left: none; }
  #app.v2 .order-card:hover { background: #eeebe3; }
  #app.v2 .order-body { gap: 5px; }
  #app.v2 .order-name { font-size: 17px; font-weight: 500; color: var(--ink); display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
  /* Ink, not gold. Gold is the scan bar's colour in this system and nothing
     else's — a gold hollow on every row spends the one signal colour on the
     most repeated element on screen. */
  #app.v2 .order-hollow { font-size: 19px; font-weight: 700; color: var(--ink); font-family: 'Space Grotesk', sans-serif; }
  #app.v2 .order-meta {
    display: flex; align-items: baseline; gap: 8px;
    font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--text-muted);
  }
  /* The shipped square is a green tint with a green glyph; in this system the
     row's action is an ink block with paper on it. The tablet rule that says so
     lives in the rail block, which portrait never enters. */
  #app.v2 .order-act {
    width: 52px; height: 52px; padding: 0;
    background: var(--ink); color: var(--paper); border: none;
  }
  #app.v2 .order-act:active { background: var(--accent); }
  #app.v2 .order-act-ico { width: 21px; height: 21px; }
  /* Same for the chips: mono, square, 1px edge, no tinted fill. */
  #app.v2 .order-code-chip, #app.v2 .skater-code-chip {
    font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.1em;
    padding: 2px 6px; background: var(--paper-3);
    border: 1px solid var(--line-chip); color: var(--text-secondary);
  }
  #app.v2 .order-team-badge, #app.v2 .order-walkin-badge {
    font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.1em;
    padding: 2px 6px; background: none; border: 1px solid var(--line-chip); color: var(--text-secondary);
  }
  /* The 🏷 fallback on a row with no code is a .skater-zap, same class as the
     customer detail's quick-sharpen — another literal rgba(77,184,212,…) that
     token remapping cannot reach. */
  #app.v2 .skater-zap {
    width: 26px; height: 26px;
    background: none; border: 1px solid var(--line-chip); color: var(--text-secondary);
  }
  /* One action per row on a phone: the 52px square. Ready keeps that — its tick
     finishes the job and there is nothing to take back. */
  #app.v2 .order-cancel { display: none; }
  /* Waiting gets a second control. What made "no room" true here was the COLUMN:
     .order-side stacks, so a second square cost a whole row of height. Turned to
     a row for these cards, the cancel sits beside the tick and costs width, and
     the 52px square still sets the row height. Cancel leads and the tick ends the
     row, the same order the dark UI's Waiting rows use. */
  #app.v2 .order-card:not(.ready) .order-side { flex-direction: row; gap: 8px; align-items: center; }
  #app.v2 .order-card:not(.ready) .order-cancel {
    display: flex; width: 40px; height: 40px; order: -1;
    /* The v2 cancel's paint lives in the tablet/desktop block, so the phone lanes
       would otherwise show the dark UI's base: a danger-red glyph in a --border
       box. In this system the resting state is a quiet outline and danger is
       reserved for the hover, where the intent is confirmed. */
    background: none; border: 1px solid var(--line-chip); color: var(--text-secondary);
  }
  #app.v2 .order-card:not(.ready) .order-cancel:hover { border-color: var(--danger); color: var(--danger); }

  /* ---- SCAN BAR, then the TAB BAR ---------------------------------------- */
  #app.v2 .scan-slab {
    left: 0; right: 0; bottom: var(--v2-tabbar-h);
    padding: 0; background: none; z-index: 50;
  }
  #app.v2 .scan-slab-btn {
    display: flex; flex-direction: row; align-items: center; justify-content: center;
    width: 100%; height: auto; padding: 20px; gap: 14px;
    background: var(--gold); color: var(--ink);
    border: none; border-top: 2px solid var(--ink);
    font-family: 'Space Grotesk', sans-serif;
  }
  #app.v2 .scan-slab-row { display: flex; align-items: center; gap: 14px; }
  #app.v2 .scan-slab-lens { display: none; }
  #app.v2 .scan-slab-qr { display: block; width: 32px; height: 32px; flex: none; }
  #app.v2 .scan-slab-title { font-size: 21px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; }
  /* 6c drops the caption on the phone — the bar is the whole width of the
     screen and the word is unambiguous at 21px. */
  #app.v2 .scan-slab-hint { display: none; }

  #app.v2 { --v2-tabbar-h: 64px; }
  #app.v2 > .view-tabs {
    display: flex; flex-direction: row; gap: 0;
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 51;
    margin: 0; padding: 0;
    background: var(--paper); border: none; border-top: 1px solid var(--ink);
    /* The bar's content came to 62px while --v2-tabbar-h, which the scan slab
       sits on and the page pads for, says 64 — so the slab floated 2px above it
       and the two bottom bars read as separated. Declaring the height makes the
       variable TRUE rather than an estimate the layout has to hope about. */
    height: var(--v2-tabbar-h);
  }
  #app.v2 .rail-brand, #app.v2 .rail-stats { display: none; }
  /* Settings and Admin are not among 6c's four. Settings still has a door — the
     header kebab keeps it on this layout (the rail block is what hides that
     item, and the rail is not up here). */
  #app.v2 #tabSettings { display: none; }
  /* `display: flex` is load-bearing. The shipped `.view-tab { display: flex }`
     lives in the rail media query, which portrait never enters, so these were
     plain block buttons — flex-direction and align-items did nothing, and the
     block-level icon sat at the LEFT EDGE while the label centred itself with
     text-align. Only #tabOrders looked right, because a separate rule makes it
     flex. Same trap as the scan slab: setting flex properties on something that
     is not a flex container. */
  #app.v2 .view-tab {
    display: flex; flex: 1;
    flex-direction: column; align-items: center; justify-content: center;
    gap: 5px; padding: 10px 0 16px; border: none; border-left: none;
    background: none; color: var(--text-secondary);
    font-family: 'DM Mono', monospace; font-size: 8px; letter-spacing: 0.06em;
    text-transform: uppercase; font-weight: 400; white-space: nowrap;
  }
  /* Letter-spacing adds a trailing gap after the last character, which pulls
     centred caps visibly left of true centre. Nudge it back by half. */
  #app.v2 .view-tab { text-indent: 0.06em; }
  #app.v2 .view-tab.active,
  #app.v2.pane-orders #tabOrders { background: var(--ink); color: var(--paper); }
  /* `.tab-ico` is display:none in the BASE sheet (index.html:628) and only the
     rail block turns it back on — so the phone's tab bar rendered as text with
     no icons at all. Both phone layouts have to restate it. */
  #app.v2 .tab-ico { display: block; width: 20px; height: 20px; opacity: 1; }
  /* The handoff's chosen Active Orders mark (wheel on blade) replaces the
     original app mark in this system. Both are in the markup; the shipped theme
     keeps the original. */
  #app.v2 #tabOrders .tab-ico:not(.tab-ico-v2) { display: none; }
  #app.v2 .tab-ico-v2 { display: block; }

  /* Clear the two fixed bars. */
  #app.v2 { padding-bottom: calc(var(--v2-tabbar-h) + 72px); }
  #toastStack { bottom: calc(var(--v2-tabbar-h) + 88px); }
}

/* ============================================================================
   PHONE LANDSCAPE — screen 7b, 844x390
   62px icon rail, one lane of rows, and a 124px gold slab down the right edge.

   ⚠️ 7b AND 7c ARE THE SAME VIEWPORT. Both are drawn at 844x390, so a phone in
   landscape has two designs competing for it and something has to choose. The
   choice lives in syncWheelMode() (see the note there) and arrives as either
   `.wheel-on` or `.phone-land`. Default is 7c, per the handoff's own
   Interactions note; `?land=queue` picks this one.

   ⚠️ 7b has NO header — no search and no + Order — while its own slab reads
   "NO TAG? SEARCH, OR USE + ORDER". The screen instructs the operator to use
   two controls it does not draw. Followed as drawn here rather than quietly
   patched, because the fix is a design decision: rotate to portrait, or give
   landscape a header row out of the 390px it has.
   ========================================================================= */
.lane-done { display: none; }

#app.phone-land { display: block; padding: 0; overflow: hidden; height: 100vh; height: 100dvh; }
#app.phone-land { --v2-rail-w: 62px; --v2-slab-w: 124px; }
#app.v2.phone-land { background: var(--paper); }
/* Only the three surfaces 7b draws. Everything else — the header, the notice
   bars, the stats strip — stays out of a 390px-tall screen. */
/* Hide the CHROME that has no place on a 390px-tall screen — named, not
   blanketed.
   ⚠️ This was `> * { display: none !important }` plus an `!important` re-show of the panes, and that second half overrode switchView's own inline display: all three panes rendered at once, each a full viewport tall, so Reports sat on screen with Customers one viewport down and Teams two. Tapping a rail chip worked perfectly and looked like it did nothing. The panes are left alone now. switchView owns which one is up, the same as every other layout. */ #app.phone-land > header.header, #app.phone-land > #onboardingCard, #app.phone-land > #stickerStockBar, #app.phone-land > #pendingBar, #app.phone-land > .stats-bar, #app.phone-land > .wheel-mode { display: none !important; }
#app.phone-land > .view-tabs, #app.phone-land .scan-slab { display: flex !important; }
/* The board is the Orders pane, so it follows the pane model rather than switchView (which only knows about the other three). */ #app.phone-land:not(.pane-orders) > #orderBoard { display: none !important; }
#app.phone-land.pane-orders > #orderBoard { display: flex !important; }
#app.phone-land.pane-orders > #customersView, #app.phone-land.pane-orders > #teamsView, #app.phone-land.pane-orders > #reportsView, #app.phone-land.pane-orders > #adminView { display: none !important; }

/* ---- ICON RAIL ---------------------------------------------------------- */ /* align-self: stretch undoes the shipped landscape block's `align-self: start`.
   The rail is a grid item there, and Chrome keeps applying that self-alignment
   to it even once this rule takes it out of flow — so top:0/bottom:0 stopped
   stretching it and `height: auto` shrink-wrapped the column to its icons.
   A 276px rail in a 360px viewport leaves #tabSettings's `margin-top: auto`
   no slack to eat, which parked the gear directly under Reports instead of in
   the bottom corner. Both themes; only a real `pointer: coarse` device (or a
   pane under 768px wide) enters the block that sets `start`, which is why this
   never showed up in a desktop A/B. */
#app.phone-land > .view-tabs, #app.wheel-on > .view-tabs { position: fixed; left: 0; top: 0; bottom: 0; width: var(--v2-rail-w); z-index: 51; flex-direction: column; align-items: center; gap: 0; padding: 7px 0; margin: 0; height: auto; align-self: stretch; border-radius: 0; }
/* The dark UI's .view-tabs is a rounded horizontal pill strip; as a full-height rail it needs a right-edge divider, not a border on four sides. */
#app:not(.v2).phone-land > .view-tabs, #app:not(.v2).wheel-on > .view-tabs { border: none; border-right: 1px solid var(--border); }
/* ---- ICON RAIL ---------------------------------------------------------- */ #app.v2.phone-land > .view-tabs, #app.v2.wheel-on > .view-tabs { background: var(--paper-2); border: none; border-right: 1px solid var(--ink); }
#app.phone-land .rail-brand, #app.wheel-on .rail-brand { display: block; padding: 0; margin-bottom: 6px; }
#app.v2.phone-land .rail-brand, #app.v2.wheel-on .rail-brand { border: none; }
#app.phone-land .rail-logo, #app.phone-land .rail-words, #app.wheel-on .rail-logo, #app.wheel-on .rail-words { display: none; }
/* The line above is (1 id, 2 classes) and the landscape block's
   `#app.v2 .rail-brand .rail-words { display: flex }` is (1 id, 3) — so the wide
   rail's lockup won, and ink & paper rendered the SKATETRACKER wordmark inside a
   62px column, running past its right edge. Re-hidden at (1 id, 4). The dark UI
   never showed it because only v2 turns the words on. */
#app.v2.phone-land .rail-brand .rail-words, #app.v2.wheel-on .rail-brand .rail-words { display: none; }
/* The mark is PAINT, not layout, and this pair was unscoped — so the dark UI's
   rail drew v2's traced ink mark instead of the blue ST it uses on every other
   dark screen. Each theme keeps its own: st-mark.png for dark, the traced glyph
   for ink & paper. The size and the slot are shared. */
#app.v2.phone-land .rail-brand .rail-mark, #app.v2.wheel-on .rail-brand .rail-mark { display: none; }
#app.v2.phone-land .rail-brand .rail-mark-ink, #app.v2.wheel-on .rail-brand .rail-mark-ink { display: block; width: 26px; height: 26px; }
#app:not(.v2).phone-land .rail-brand .rail-mark, #app:not(.v2).wheel-on .rail-brand .rail-mark { display: block; width: 26px; height: 26px; }
#app.phone-land .rail-stats, #app.wheel-on .rail-stats { display: none; }
/* 50x46 hit chips: icon only, no label. The label would not fit and the rail is four destinations the operator already knows by shape. */ #app.phone-land .view-tab, #app.wheel-on .view-tab { flex: none; width: 50px; height: 46px; padding: 0; gap: 0; display: flex; align-items: center; justify-content: center; font-size: 0; }
/* 50x46 hit chips: icon only, no label. The label would not fit and the rail is four destinations the operator already knows by shape. */ #app.v2.phone-land .view-tab, #app.v2.wheel-on .view-tab { border: none; background: none; color: var(--text-secondary); }
#app.v2.phone-land .view-tab:hover,
#app.v2.wheel-on .view-tab:hover { background: var(--v2-hover); }
#app.v2.phone-land .view-tab.active,
#app.v2.wheel-on .view-tab.active,
#app.v2.phone-land.pane-orders #tabOrders,
#app.v2.wheel-on.pane-orders #tabOrders { background: var(--ink); color: var(--paper); }
#app.v2.phone-land.pane-orders .view-tab.active,
#app.v2.wheel-on.pane-orders .view-tab.active { background: none; color: var(--text-secondary); }
#app.phone-land #tabOrders, #app.wheel-on #tabOrders { display: flex; }
#app.phone-land #tabSettings, #app.wheel-on #tabSettings { display: flex; margin-top: auto; padding: 0; }
/* `font-size: 0` is what makes this rail icon-only, but it is carried by
   `#app.phone-land .view-tab` at (1 id, 2 classes) and the landscape block's
   `#app.v2 #tabSettings` is TWO IDs — so Settings alone kept its 13px label and
   painted it outside a 50px button in a 62px rail. The other four tabs are only
   class-addressed there, which is why this was the one row that leaked. */
#app.v2.phone-land #tabSettings, #app.v2.wheel-on #tabSettings { border: none; font-size: 0; }
/* Same base rule as portrait: .tab-ico is display:none unless the rail block turns it on, and an icon-ONLY rail with no icons is an empty column. */ #app.phone-land .tab-ico:not(.tab-ico-v2), #app.wheel-on .tab-ico:not(.tab-ico-v2) { display: block; width: 21px; height: 21px; }
/* .tab-ico-v2 carries .tab-ico too, so the rail's display:block would reveal the traced ink glyph in the dark UI as a SECOND icon. Only v2 turns it on (below). */
#app.v2.phone-land .tab-ico-v2, #app.v2.wheel-on .tab-ico-v2 { width: 21px; height: 21px; }
/* Same base rule as portrait: .tab-ico is display:none unless the rail block turns it on, and an icon-ONLY rail with no icons is an empty column. */ #app.v2.phone-land .tab-ico, #app.v2.wheel-on .tab-ico { opacity: 1; }
/* Unscoped: Active Orders was the ONE rail row whose glyph differed between the
   themes — dark kept #ico-skate (an 84x88 boot) while ink & paper swapped to
   #ico-skate-c, the 24x24 traced glyph drawn on the same grid as the other
   three. One rail, one icon set. The :not() on the display:block above is what
   keeps this from showing BOTH, which is what scoped it to v2 in the first
   place. */
#app.phone-land #tabOrders .tab-ico:not(.tab-ico-v2), #app.wheel-on #tabOrders .tab-ico:not(.tab-ico-v2) { display: none; }
#app.phone-land .tab-ico-v2, #app.wheel-on .tab-ico-v2 { display: block; width: 21px; height: 21px; }
#app.phone-land #tabSettings .tab-ico, #app.wheel-on #tabSettings .tab-ico { width: 19px; height: 19px; }

/* ---- THE MIDDLE COLUMN --------------------------------------------------- */ /* The middle column. Layout only — NO `display`, so switchView stays in charge of which pane is showing. */ #app.phone-land > #orderBoard, #app.phone-land > #customersView, #app.phone-land > #teamsView, #app.phone-land > #reportsView, #app.phone-land > #adminView { flex-direction: column; min-height: 0; margin-left: var(--v2-rail-w); margin-right: var(--v2-slab-w); height: 100vh; height: 100dvh; overflow-y: auto; padding: 0; }
/* Customers and Teams keep their list/record split; at 390px tall the list has to scroll inside itself rather than push the record off the screen. */ #app.phone-land .cust-layout { height: auto; flex: 1; min-height: 0; }
#app.phone-land #customersView .toolbar { display: flex; flex-direction: row; align-items: center; gap: 10px; flex-wrap: nowrap; margin: 0; padding: 8px 14px; }
#app.v2.phone-land #customersView .toolbar { border-bottom: 1px solid var(--line); background: var(--paper-3); }
#app.phone-land #customersView .list-count { display: none; }
#app.phone-land #orderBoard .section-head { display: none; }

/* The status strip. 7b flattens the segmented switch into one line: the open lane, its count, then the other lane and the day as readouts. Both halves stay TAPPABLE — the design draws Ready as text, but it is the same control underneath and making it dead would be a regression on the shipped phone. */ /* ── 3 ── A TWO-STATE CONTROL, NOT TWO READOUTS ─────────────────────────── 7b draws Ready as plain text. Both halves have always been buttons and the tap always worked — but nothing said so, so the strip read as a status line and the second lane looked unreachable. The fix is the treatment this system already uses for "this one is active" in the nav rail and in the portrait segments: ink fill, paper text. The pair is bound by one ink hairline with a divider between, which is what makes it read as ONE control with two positions rather than two separate chips. */ /* `display: flex` belongs HERE, not on the .v2 twin below. The shipped sheet
   says `.lane-seg { display: none }` and only lifts it under (max-width: 640px)
   — so at 740px landscape the dark UI never got the strip back and lost the
   Waiting/Ready control, the team filter chips renderQueueFilterBar moves INTO
   it, and the done count. Same trap as the rail's `font-size: 0`: a `display`
   that decides whether a whole row exists is layout, and layout is shared. */
#app.phone-land .lane-seg { display: flex; align-items: center; gap: 0; flex: none; margin: 0; padding: 6px 14px; padding-right: 120px; }
/* The status strip. 7b flattens the segmented switch into one line: the open lane, its count, then the other lane and the day as readouts. Both halves stay TAPPABLE — the design draws Ready as text, but it is the same control underneath and making it dead would be a regression on the shipped phone. */ /* ── 3 ── A TWO-STATE CONTROL, NOT TWO READOUTS ─────────────────────────── 7b draws Ready as plain text. Both halves have always been buttons and the tap always worked — but nothing said so, so the strip read as a status line and the second lane looked unreachable. The fix is the treatment this system already uses for "this one is active" in the nav rail and in the portrait segments: ink fill, paper text. The pair is bound by one ink hairline with a divider between, which is what makes it read as ONE control with two positions rather than two separate chips. */ #app.v2.phone-land .lane-seg { /* gap 0: the two halves must TOUCH to read as one control. Spacing before
     the done count comes from .lane-done's auto margin instead. */
  display: flex; border: none; border-radius: 0; background: var(--paper-3); border-bottom: 1px solid var(--line); }
#app.phone-land .lane-seg-opt { flex: none; min-height: 0; gap: 6px; padding: 4px 10px; }
#app.v2.phone-land .lane-seg-opt { border: 1px solid var(--ink); background: none; color: var(--text-secondary); font-family: 'DM Mono', monospace; font-size: 10px; font-weight: 400; letter-spacing: 0.16em; text-transform: uppercase; cursor: pointer; }
/* Joined into one object: the two halves share the seam. */
#app.v2.phone-land .lane-seg-opt.waiting { border-right-width: 0; }
#app.v2.phone-land .lane-seg-opt:hover { background: #e0dcd2; color: var(--ink); }
#app.v2.phone-land .lane-seg-opt.active {
  background: var(--ink) !important; color: var(--paper) !important;
}
/* ── THE DARK LANE CONTROL ────────────────────────────────────────────────
   Every declaration for .lane-seg-opt and .lane-seg-count in the shipped sheet
   sits inside its (max-width: 640px) block, so at 740px landscape the dark UI
   got NONE of it and both halves fell back to raw UA buttons.

   Given each half its own rounded chip and a tint, the pair read as two STATS
   rather than one control — nothing said the other half was somewhere you could
   go. So this follows .ui-choice, the segmented control the dark UI already uses
   in Settings > Appearance: one hairline around the pair, square where they
   meet, no gap, and the divider carried by the second half's left border. That
   is what makes two buttons read as one switch with two positions.

   The fill is the lane's own colour rather than .ui-choice's accent, because
   gold=waiting and green=ready is language this board already speaks — the
   counts, the queue cards and the empty states all use it. Solid, not tinted:
   at 11px a 14%-alpha wash is not a strong enough "you are here". */
#app:not(.v2).phone-land .lane-seg-opt, #app:not(.v2).wheel-on .lane-seg-opt {
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); background: transparent; color: var(--text-muted);
  font-family: 'DM Mono', monospace; font-size: 11px; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase; cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
/* Square where they meet; the divider is ready's left border, so the pair shares
   ONE line rather than stacking two. */
#app:not(.v2).phone-land .lane-seg-opt.waiting, #app:not(.v2).wheel-on .lane-seg-opt.waiting { border-radius: 8px 0 0 8px; border-right: none; }
#app:not(.v2).phone-land .lane-seg-opt.ready, #app:not(.v2).wheel-on .lane-seg-opt.ready { border-radius: 0 8px 8px 0; }
#app:not(.v2).phone-land .lane-seg-opt:hover, #app:not(.v2).wheel-on .lane-seg-opt:hover { color: var(--text-primary); }
#app:not(.v2).phone-land .lane-seg-count, #app:not(.v2).wheel-on .lane-seg-count {
  display: inline-flex; align-items: center; justify-content: center;
  border: none; border-radius: 8px; background: rgba(120,150,180,0.14);
  color: inherit; font-family: 'DM Mono', monospace; font-size: 11px; font-weight: 700;
}
/* The fill is the lane colour at 50%, not solid: at full strength a block of
   gold or green this size out-shouted the board it labels. The hairline stays
   SOLID so the shape still reads crisply, and the text goes light — over a
   half-strength fill on a dark ground the near-black text fell to about 3.5:1,
   under the 4.5 a 11px label needs. */
#app:not(.v2).phone-land .lane-seg-opt.waiting.active, #app:not(.v2).wheel-on .lane-seg-opt.waiting.active { background: rgba(232,200,74,0.5); border-color: var(--gold); color: var(--text-primary); }
#app:not(.v2).phone-land .lane-seg-opt.ready.active, #app:not(.v2).wheel-on .lane-seg-opt.ready.active { background: rgba(77,200,122,0.5); border-color: var(--success); color: var(--text-primary); }
/* On a filled half the count needs to sit ON the fill, so it darkens instead of
   carrying its own steel wash — which on gold would read as a third colour. */
#app:not(.v2).phone-land .lane-seg-opt.active .lane-seg-count, #app:not(.v2).wheel-on .lane-seg-opt.active .lane-seg-count { background: rgba(0,0,0,0.22); }
#app.phone-land .lane-seg-opt.ready { margin-left: 0; }
#app.v2.phone-land .lane-seg-opt.ready { letter-spacing: 0.16em; }
#app.phone-land .lane-seg-count { min-width: 0; height: auto; padding: 1px 6px; }
#app.v2.phone-land .lane-seg-count { border-radius: 0; font-family: 'DM Mono', monospace; font-size: 10px; font-weight: 400; background: none; border: 1px solid var(--line-chip); color: var(--text-secondary); }
/* The active half's count takes its lane's colour — gold for Waiting, green
   for Ready, the pair the board already uses. */
#app.v2.phone-land .lane-seg-opt.waiting.active .lane-seg-count {
  background: var(--gold); border-color: var(--gold); color: var(--ink);
}
#app.v2.phone-land .lane-seg-opt.ready .lane-seg-count { border-color: var(--success); color: var(--success); }
#app.v2.phone-land .lane-seg-opt.ready.active .lane-seg-count {
  background: var(--success); border-color: var(--success); color: var(--paper);
}


/* The team filter, riding inside the strip (moved there by renderQueueFilterBar). Smaller than its own-row form and squared like every other chip in this system. It takes the slack between the lane control and the done count, and scrolls sideways rather than wrapping — a wrap would grow the strip and hand back the row this move just reclaimed. */ #app.phone-land .lane-seg .queue-filter-bar { display: flex !important; flex: 1; min-width: 0; gap: 5px; margin: 0 0 0 12px; padding: 0; flex-wrap: nowrap; overflow-x: auto; }
/* The team filter, riding inside the strip (moved there by renderQueueFilterBar). Smaller than its own-row form and squared like every other chip in this system. It takes the slack between the lane control and the done count, and scrolls sideways rather than wrapping — a wrap would grow the strip and hand back the row this move just reclaimed. */ #app.v2.phone-land .lane-seg .queue-filter-bar { scrollbar-width: none; }
#app.phone-land .lane-seg .queue-filter-bar::-webkit-scrollbar { display: none; }
#app.phone-land .qf-chip { flex: none; padding: 3px 8px; }
#app.v2.phone-land .qf-chip { font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; border-radius: 0; background: none; border: 1px solid var(--line-chip); color: var(--text-secondary); white-space: nowrap; }
#app.v2.phone-land .qf-chip:hover { border-color: var(--ink); color: var(--ink); }
/* Filled ink when a team is selected — the same "this one is active" the lane
   control and the nav rail use, so the two controls in this strip agree. */
#app.v2.phone-land .qf-chip.active {
  background: var(--ink); border-color: var(--ink); color: var(--paper); font-weight: 400;
}
/* The done count is a READOUT; the team filter is a CONTROL. At 844px they cannot both have the strip's middle, and on the screen with the least room a control wins. The number is still on the portrait header and in the tablet's rail foot, so nothing is lost — it just isn't shown at the machine. (One rule, not an override: a `display: none` written earlier in this file loses to this one on source order, which is exactly the bug that hid it.) */ #app.phone-land .lane-done { display: none; }

/* ---- ROWS ---------------------------------------------------------------- */ #app.phone-land .queue-grid { display: block; gap: 0; flex: 1; min-height: 0; overflow-y: auto; }
/* ---- ROWS ---------------------------------------------------------------- */ #app.v2.phone-land .queue-grid { background: none; }
#app.phone-land .queue-card, #app.phone-land .queue-card.waiting.has-items, #app.phone-land .queue-card.ready.has-items { padding: 0; height: auto; }
#app.v2.phone-land .queue-card, #app.v2.phone-land .queue-card.waiting.has-items, #app.v2.phone-land .queue-card.ready.has-items { background: none; border: none; }
/* ── 2 ── THE DUE/ROH SWITCH IS NOT OPTIONAL HERE ───────────────────────── Hiding .queue-top wholesale took the sort switch with it, and landscape at the machine is exactly where ROH batching earns its keep — it is the whole reason the sort exists. Lifted INTO the status strip by absolute position rather than given a row of its own: 390px of height has none to spare, and the strip already has the space at its right end. */ #app.phone-land #orderBoard { position: relative; }
#app.phone-land .queue-top { position: absolute; top: 0; right: 14px; height: 33px; z-index: 2; display: flex; align-items: center; margin: 0; padding: 0; }
#app.v2.phone-land .queue-top { background: none; border: none; }
#app.phone-land #readyCard .queue-top { display: none; }  /* Ready has no sort */ #app.phone-land .queue-label, #app.phone-land .count-badge { display: none; }           /* the strip names the lane */ #app.phone-land .sort-switch { padding: 0; }
/* the strip names the lane */ #app.v2.phone-land .sort-switch { border: 1px solid var(--ink); background: none; }
#app.phone-land .sort-opt { padding: 3px 8px; z-index: 1; }
#app.v2.phone-land .sort-opt { font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-secondary); }
#app.v2.phone-land .sort-thumb { background: var(--ink) !important; }
#app.v2.phone-land .sort-opt.active { color: var(--paper); }
/* ONE LANE AT A TIME. The shipped rule that does this lives in the phone's `max-width: 640px` block, which an 844px-wide landscape phone never enters — so both lanes rendered, stacked, and the strip's "Waiting 3" sat above a list that also held the Ready orders. Same rule, restated for this width. */ #app.phone-land #orderBoard:not(.lane-ready) #readyCard, #app.phone-land #orderBoard.lane-ready #waitingCard { display: none; }
/* The 46px second column is what puts the complete-sharpen button BESIDE the
   row instead of under it. Stranded on .v2, the dark card was a one-column grid,
   so the button wrapped to its own line at the bottom left and every row cost an
   extra ~56px — the one thing a 390px-tall screen cannot spend. The button was
   already 46x46 unscoped; only the track it sits in was missing. */
#app.phone-land .order-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: center; padding: 10px 14px; margin: 0; }
#app.v2.phone-land .order-card { background: none; border: none; border-bottom: 1px solid var(--line-soft); }
#app.v2.phone-land .order-card:hover { background: #eeebe3; }
#app.phone-land .order-body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
#app.phone-land .order-name { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; margin: 0; }
#app.v2.phone-land .order-name { font-size: 15px; font-weight: 500; color: var(--ink); }
/* The whole row opens the job, so the whole row is the target — not the name,
   which is one 20px line and misses more often than it hits. The name still
   gets the underline on hover because that is where the eye is, but the
   pointer and the hit area belong to the row. */
#app.v2.phone-land .order-card { cursor: pointer; }
#app.v2.phone-land .order-card:hover .order-name { text-decoration: underline; text-underline-offset: 3px; }
/* The controls inside keep their own affordance and their own jobs. */
#app.v2.phone-land .order-act,
#app.v2.phone-land .skater-zap { cursor: pointer; }
#app.v2.phone-land .order-hollow {
  font-size: 16px; font-weight: 700; color: var(--ink); font-family: 'Space Grotesk', sans-serif;
}
#app.phone-land .order-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin: 0; }
#app.v2.phone-land .order-meta { font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-muted); }
#app.phone-land .order-code-chip, #app.phone-land .skater-code-chip, #app.phone-land .order-team-badge, #app.phone-land .order-walkin-badge { padding: 1px 5px; }
#app.v2.phone-land .order-code-chip, #app.v2.phone-land .skater-code-chip, #app.v2.phone-land .order-team-badge, #app.v2.phone-land .order-walkin-badge { font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.08em; background: var(--paper-3); border: 1px solid var(--line-chip); color: var(--text-secondary); }
#app.phone-land .skater-zap { width: 24px; height: 24px; }
#app.v2.phone-land .skater-zap { background: none; border: 1px solid var(--line-chip); color: var(--text-secondary); }
#app.phone-land .order-act { width: 46px; height: 46px; padding: 0; }
#app.v2.phone-land .order-act { background: var(--ink); color: var(--paper); border: none; }
#app.v2.phone-land .order-act:active { background: var(--accent); }
#app.phone-land .order-act-ico { width: 19px; height: 19px; }
/* Ink & paper only. Its .order-side is a COLUMN, so a second control there costs
   a whole row of height — which is what "no room at 390px tall" meant. The dark
   UI's .order-side is a ROW (index.html), so its cancel sits BESIDE the tick and
   costs width, not height; the track above is `auto` so it has that width. This
   was scoped to v2 when it was written and got unscoped in the landscape port,
   which is where the dark UI lost its cancel. */
#app.v2.phone-land .order-cancel { display: none; }
/* Same as portrait: the column is what cost a row, so Waiting rows lay .order-side
   out sideways and gain the cancel. 38px against the tick's 46px — same row, but
   the destructive one is not the equal of the one being reached for. The action
   track is `auto`, so it takes the pair without a further change. */
#app.v2.phone-land .order-card:not(.ready) .order-side, #app.v2.wheel-on .order-card:not(.ready) .order-side { flex-direction: row; gap: 8px; align-items: center; }
#app.v2.phone-land .order-card:not(.ready) .order-cancel, #app.v2.wheel-on .order-card:not(.ready) .order-cancel { display: flex; width: 38px; height: 38px; order: -1; background: none; border: 1px solid var(--line-chip); color: var(--text-secondary); }
#app.v2.phone-land .order-card:not(.ready) .order-cancel:hover, #app.v2.wheel-on .order-card:not(.ready) .order-cancel:hover { border-color: var(--danger); color: var(--danger); }
/* Smaller than the tick it sits next to: same row, but the destructive one should
   not read as the equal of the one the operator means to hit. */
#app:not(.v2).phone-land .order-cancel, #app:not(.v2).wheel-on .order-cancel { display: flex; width: 38px; height: 38px; font-size: 13px; }
/* Waiting rows lead with the cancel and END on the tick. DOM order is tick then
   cancel — right for Ready, where the tick is "picked up" and finishes the job —
   but on the Waiting side the tick is the one the operator reaches for all shift,
   and it should sit where the thumb lands rather than have the destructive
   control occupy the outer edge. Swapped with `order`, not in the markup, so the
   tab order still runs primary-then-destructive.
   Both lanes: Ready carries a cancel in the dark UI too, and a destructive
   control that changes sides when the lane changes is worse than one in a
   slightly odd spot. Dark only — ink & paper's Ready shows no cancel to place. */
#app:not(.v2).phone-land .order-cancel,
#app:not(.v2).wheel-on .order-cancel { order: -1; }
@media (max-width: 640px) {
  #app:not(.v2) .order-cancel { order: -1; }
}

/* ---- THE SLAB ------------------------------------------------------------ Full height on the RIGHT edge, not a bar along the bottom. This is where the thumb rests when a phone is held in landscape, and it is the reason this screen is worth having at all. */ #app.phone-land .scan-slab { position: fixed; right: 0; top: 0; bottom: 0; left: auto; width: var(--v2-slab-w); z-index: 52; padding: 0; }
/* ---- THE SLAB ------------------------------------------------------------ Full height on the RIGHT edge, not a bar along the bottom. This is where the thumb rests when a phone is held in landscape, and it is the reason this screen is worth having at all. */ #app.v2.phone-land .scan-slab { background: none; }
#app.phone-land .scan-slab-btn { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; width: 100%; height: 100%; padding: 14px 10px; }
#app.v2.phone-land .scan-slab-btn { background: var(--gold); color: var(--ink); border: none; border-left: 2px solid var(--ink); font-family: 'Space Grotesk', sans-serif; }
#app.v2.phone-land .scan-slab-btn:active { background: var(--gold-hover); }
#app.phone-land .scan-slab-row { display: contents; }
#app.phone-land .scan-slab-lens { display: none; }
#app.phone-land .scan-slab-qr { display: block; width: 44px; height: 44px; flex: none; }
#app.v2.phone-land .scan-slab-title { font-size: 26px; font-weight: 700; letter-spacing: 0.02em; text-transform: none; }
#app.phone-land .scan-slab-hint { display: block; }
#app.v2.phone-land .scan-slab-hint { font-family: 'DM Mono', monospace; font-size: 9px; font-weight: 500; letter-spacing: 0.09em; line-height: 1.4; text-align: center; text-transform: uppercase; color: var(--gold-ink); opacity: 1; }

/* ============================================================================
   TABLET PORTRAIT — the one layout nothing ever addressed
   The rail lane wants landscape or >=1000px, the phone lane wants <=640px, and
   phone-land wants a landscape phone. An iPad held upright (768-1024px, coarse,
   portrait) matches none of them — yet the shipped slab's display gate has no
   orientation clause, so the bar is up and, until the paint moved out to every
   width above, it was up in the wrong colour.

   Paint is already handled. This is only the SHAPE: the same full-bleed bottom
   bar the phone gets, anchored at 0 rather than above a tab bar, because
   portrait on a tablet keeps its tabs inline on the board.
   ========================================================================= */
@media (pointer: coarse) and (hover: none) and (orientation: portrait) and (min-width: 641px) {
  /* Portrait is the tablet's queue-first mode: one full-width lane at a time.
     The selector is shared with the phone, but has tablet-sized hit targets and
     the paper system's ink/rule treatment rather than a rounded phone control. */
  #app.v2 .queue-grid { grid-template-columns: minmax(0, 1fr); }
  #app.v2 .lane-seg {
    display: grid; grid-template-columns: 1fr 1fr; gap: 0;
    margin: 0 16px 12px; padding: 0;
    background: none; border: 1px solid var(--ink);
  }
  #app.v2 .lane-seg-opt {
    display: flex; align-items: center; justify-content: center; gap: 9px;
    min-height: 48px; padding: 0 16px;
    background: none; border: 0; color: var(--text-secondary);
    font-family: 'DM Mono', monospace; font-size: 11px; font-weight: 400;
    letter-spacing: 0.14em; text-transform: uppercase;
  }
  #app.v2 .lane-seg-opt + .lane-seg-opt { border-left: 1px solid var(--ink); }
  #app.v2 .lane-seg-opt.active { background: var(--ink); color: var(--paper); }
  #app.v2 .lane-seg-count {
    min-width: 24px; height: 24px; padding: 0 7px;
    background: none; border: 1px solid var(--line-chip); border-radius: 0;
    color: currentColor; font-family: 'DM Mono', monospace; font-size: 10px; font-weight: 400;
  }
  #app.v2 .lane-seg-opt.active .lane-seg-count { border-color: var(--paper); }
  #app.v2 #orderBoard:not(.lane-ready) #readyCard,
  #app.v2 #orderBoard.lane-ready #waitingCard { display: none; }
  #app.v2 .queue-card .queue-label, #app.v2 .queue-card .count-badge { display: none; }
  #app.v2 #waitingCard .queue-top { justify-content: flex-end; margin-bottom: 8px; }
  #app.v2 #readyCard .queue-top { display: none; }

  #app.v2 .scan-slab { left: 0; right: 0; bottom: 0; }
  #app.v2 .scan-slab-btn {
    flex-direction: row; align-items: center; justify-content: center;
    width: 100%; height: auto; padding: 20px; gap: 18px;
  }
  #app.v2 .scan-slab-row { display: flex; align-items: center; gap: 18px; }
  #app.v2 .scan-slab-qr { width: 36px; height: 36px; }
  #app.v2 .scan-slab-title { font-size: 22px; }
  /* Dropped for the same reason the phone drops it: the bar is the full width
     of the screen and the word is unambiguous at 22px. */
  #app.v2 .scan-slab-hint { display: none; }

  /* ---- THE BOARD ---------------------------------------------------------
     Paint that both designed lanes agreed on is hoisted to every width above.
     What is left here is this lane's own: the sizes, and the handful of values
     the rail and the phone chose DIFFERENTLY, which therefore had to be picked
     rather than inherited. Touch sizing throughout — a coarse pointer reaches
     this board with a finger, so the phone's 52px is the right square, not the
     rail's 44px. */
  /* `border: none` has to restate the bottom rule, or the shorthand wipes the
     one hoisted above — the rail's own rule is written this way for the same
     reason. Dropping it silently un-ruled the rows. */
  #app.v2 .order-card {
    border: none; border-bottom: 1px solid var(--line-soft);
    margin: 0; padding: 14px 16px;
  }
  #app.v2 .order-act { width: 52px; height: 52px; }
  #app.v2 .order-hollow { font-size: 19px; }
  #app.v2 .order-meta {
    font-family: 'DM Mono', monospace; letter-spacing: 0.06em;
    color: var(--text-secondary);
  }
  /* The rail distinguishes the two badges — accent for a team, a dashed edge
     for a walk-in — where the phone flattens both to one neutral outline. Keep
     the distinction: portrait has the width for it, and the two mean different
     things. */
  #app.v2 .order-team-badge, #app.v2 .order-walkin-badge { text-transform: uppercase; }
  #app.v2 .order-team-badge { border: 1px solid var(--accent); color: var(--accent); }
  #app.v2 .order-walkin-badge { border: 1px dashed #a09a8c; color: var(--text-muted); }
  /* Squared, like the rail's — a pill is the shipped shape, not this system's.
     Stated here because the phone lane deliberately keeps its pill. */
  #app.v2 .sort-thumb { border-radius: 0 !important; }
  #app.v2 .sort-opt { font-family: 'DM Mono', monospace; }
  #app.v2 .queue-label { font-size: 11px; font-weight: 400; letter-spacing: 0.18em; }

  /* ---- THE TAB ROW -------------------------------------------------------
     Neither designed lane helps here: the rail's tabs are a vertical column
     and the phone's are a fixed bottom bar, while portrait keeps the shipped
     HORIZONTAL row inline on the board. So it is stated here, and it is the
     one place this lane invents rather than inherits.

     The shipped row is a rounded box holding a filled pill — `.view-tab.active
     { background: var(--accent) }`, which under v2's palette is the teal seen
     on a paper board. This system marks the live item with a rule and ink, not
     a fill: the strip becomes a hairline, the labels mono caps, and the active
     tab takes a 2px ink underline — the same weight the modal header and the
     rail brand use to say "this is a thing, not a row".

     No `.pane-orders` special-casing: the pane model belongs to the rail lane,
     so in portrait `.view-tab.active` means simply the view you are on. */
  #app.v2 > .view-tabs {
    background: none; border: none; border-bottom: 1px solid var(--line);
    border-radius: 0; padding: 0; gap: 0; margin-bottom: 14px;
  }
  #app.v2 .view-tab {
    flex: none; padding: 11px 16px;
    font-family: 'DM Mono', monospace; font-size: 10px; font-weight: 400;
    letter-spacing: 0.14em; text-transform: uppercase;
    background: none; color: var(--text-secondary);
    border: none; border-bottom: 2px solid transparent;
  }
  #app.v2 .view-tab:hover { background: none; color: var(--ink); }
  #app.v2 .view-tab.active {
    background: none; color: var(--ink); border-bottom-color: var(--ink);
  }
  /* Admin keeps its gold, as it does in the rail — it is the one destination
     that is not this shop's own. */
  #app.v2 #tabAdmin, #app.v2 #tabAdmin.active { color: var(--gold-ink); }

  /* ⚠️ The Settings tab is hidden here, and it takes an explicit rule to do it.
     The shipped sheet hides #tabOrders and #tabSettings outside the rail
     layouts, but the STRUCTURAL block at the top of this file says
     `#app.v2 .view-tab { display: flex }` — unscoped, to fix the flex-box trap —
     and at (1,1,0) that outranks `#tabOrders` at (1,0,0). So v2 was resurrecting
     both tabs in portrait: a dead Active Orders (the board was always on, so it
     switched nothing) and a Settings tab duplicating the ⋯ menu's own entry,
     against this app's one-control-per-destination rule. Today's tab-row styling
     then dressed them as legitimate tabs, which is what made them read as broken
     buttons rather than as leftovers.

     Active Orders now earns its place — the portrait pane model in index.html
     gives it a board to switch to. Settings does not: the ⋯ menu is its door. */
  #app.v2 #tabSettings { display: none; }

  /* With the board a pane, its tab is the lit one while it is showing. */
  #app.v2.pane-orders .view-tab.active {
    background: none; color: var(--text-secondary); border-bottom-color: transparent;
  }
  #app.v2.pane-orders #tabOrders {
    background: none; color: var(--ink); border-bottom-color: var(--ink);
  }

  /* ---- THE LANE PANEL ----------------------------------------------------
     The selected lane fills the portrait board. Its header takes the paper-3
     band and a rule under it, so the list reads as a work queue rather than a
     floating card. Landscape retains the two-column work-station. */
  #app.v2 .queue-grid { gap: 1px; background: var(--line); }
  #app.v2 .queue-top {
    padding: 13px 16px; margin: 0;
    background: var(--paper-3); border-bottom: 1px solid var(--line);
  }
}

/* ============================================================================
   CUSTOMER-FACING PAGES — ink and paper
   beta.html · my-account.html · my-team.html · new-customer.html · new-team.html

   These five share ONE palette and ONE class vocabulary (beta.html's :root says
   so out loud), so converting the primitives here converts all of them at once.
   Scoped to `body.v2-on`, which shared.js sets on every page.

   Not in the Aug 31 handoff — that covers the operator app only. This extends
   the same system outward: ink on paper, 1px rules, radius 0, no shadows, mono
   for labels and Space Grotesk for everything else.
   ========================================================================= */

body.v2-on {
  /* The pages use --ink and --steel as BACKGROUNDS (darkest thing on screen).
     Inverted here to the lightest, so every `background: var(--ink)` keeps
     meaning "the page" rather than turning a paper page black. */
  --ink:            #16150f;
  --steel:          #f4f2ed;
  --steel-mid:      #efece5;
  --steel-light:    #e9e6de;
  --ice:            #f4f2ed;
  --accent:         #246c80;
  --accent-bright:  #16150f;
  --accent-deep:    #246c80;
  --gold:           #e8c84a;
  --gold-deep:      #5f531c;
  --success:        #2f6b45;
  --danger:         #c8352a;
  --text-primary:   #16150f;
  --text-secondary: #57534a;
  /* ⚠️ The customer-facing rules below mostly write this colour as a LITERAL
     rather than through the token, so changing the token alone would have left
     22 of them at the old failing value. They were moved with it. */
  --text-muted:     #67635b;   /* see the note on the operator token above */
  --text-placeholder: #a09a8c;
  --border:         #d8d4ca;
  --border-hover:   #16150f;
  --card-bg:        #efece5;
  --surface:        #fbfaf7;
  --shadow:         none;

  background: #f4f2ed;
  color: #16150f;
  font-family: 'Space Grotesk', 'Geom', Helvetica, sans-serif;
}

/* The aurora wash is the signature of the theme being replaced — two fixed
   radial gradients behind everything. Paper has no glow. */
body.v2-on::before, body.v2-on::after { display: none !important; }

/* Radius and shadow, everywhere, once. The avatar/crest circles opt back in
   below, the same exception the operator app makes. */
body.v2-on .wrap *, body.v2-on .topbar * { border-radius: 0 !important; }
body.v2-on * { box-shadow: none !important; backdrop-filter: none !important; }
body.v2-on .crest, body.v2-on .avatar, body.v2-on .k-dot,
body.v2-on .perk-ic { border-radius: 50% !important; }

/* ---- SHELL --------------------------------------------------------------- */
body.v2-on .topbar {
  padding: 16px 4px 14px; border-bottom: 1px solid var(--ink); margin-bottom: 4px;
}
body.v2-on .topbar-tag {
  font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.14em;
  text-transform: uppercase; padding: 3px 8px;
  background: var(--ink); color: #f4f2ed; border: none;
}
body.v2-on .sec-head { margin: 22px 4px 10px; }
body.v2-on .sec-head h2 {
  font-family: 'DM Mono', monospace; font-size: 11px; font-weight: 400;
  letter-spacing: 0.18em; text-transform: uppercase; color: var(--text-secondary);
}

/* ---- HERO ---------------------------------------------------------------- */
/* A stamped block, not a glowing pass: flat paper-2 with a 2px ink edge — the
   same weight the operator app's rail brand and record header use. */
body.v2-on .pass {
  background: var(--paper-2, #efece5) !important;
  border: 1px solid var(--ink); padding: 22px;
}
body.v2-on .pass::before, body.v2-on .pass::after { display: none !important; }
body.v2-on .pass.welcome {
  border-top: 2px solid var(--ink); border-bottom: 2px solid var(--ink);
  padding: 26px 22px 22px;
}
body.v2-on .pass-kicker {
  background: none !important; border: 1px solid var(--ink) !important;
  padding: 4px 10px; color: var(--ink);
  font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.16em;
}
body.v2-on .k-dot { background: var(--gold); animation: none !important; }
body.v2-on .pass.welcome h1 { color: var(--ink); letter-spacing: -0.02em; }
body.v2-on .pass.welcome p { color: var(--text-secondary); }

/* ---- CARDS + FORMS ------------------------------------------------------- */
body.v2-on .card {
  background: var(--paper-2, #efece5); border: 1px solid var(--ink); padding: 20px;
}
body.v2-on .form-label {
  font-family: 'DM Mono', monospace; font-size: 10px; font-weight: 400;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-secondary);
}
body.v2-on .form-input, body.v2-on select, body.v2-on textarea {
  background: #fbfaf7; border: 1px solid var(--ink); color: var(--ink);
  font-family: 'Space Grotesk', sans-serif; padding: 11px 13px;
}
body.v2-on .form-input:focus, body.v2-on select:focus, body.v2-on textarea:focus {
  border-color: var(--accent); outline: 2px solid rgba(36,108,128,0.25); outline-offset: -2px;
}
body.v2-on .form-input::placeholder { color: var(--text-placeholder); }
body.v2-on .form-hint { font-size: 11.5px; color: var(--text-secondary); }

/* ---- BUTTONS ------------------------------------------------------------- */
/* One filled control per screen, in ink. Gold stays reserved for Scan in the
   operator app and for the loyalty accents here — a gold primary button would
   spend the signal colour on every page. */
body.v2-on .btn {
  font-family: 'Space Grotesk', sans-serif; font-weight: 500; font-size: 15px;
  padding: 14px 20px; border: 1px solid var(--ink); transition: background 0.15s;
}
body.v2-on .btn-primary, body.v2-on .btn.primary {
  background: var(--ink) !important; color: #f4f2ed !important; border-color: var(--ink);
}
body.v2-on .btn-primary:hover, body.v2-on .btn.primary:hover { background: var(--accent) !important; border-color: var(--accent); }
body.v2-on .btn-ghost, body.v2-on .btn.secondary {
  background: none !important; color: var(--ink) !important; border-color: var(--ink);
}
body.v2-on .btn-ghost:hover, body.v2-on .btn.secondary:hover { background: #e6e2d8 !important; }

/* ---- PERKS --------------------------------------------------------------- */
body.v2-on .perk { border-bottom: 1px solid #e2ded4; padding: 14px 0; }
body.v2-on .perk:last-child { border-bottom: none; }
body.v2-on .perk-ic {
  background: var(--paper-3, #e9e6de) !important; border: 1px solid #cfcabd;
  color: var(--ink); filter: grayscale(1);
}
body.v2-on .perk-body h3, body.v2-on .perk-title { color: var(--ink); }
body.v2-on .perk-body p, body.v2-on .perk-sub { color: var(--text-secondary); }

/* ---- THE LOCKUP ----------------------------------------------------------
   logo.png is a lockup whose wordmark is WHITE, so on paper only the blue ST
   survives and the name disappears. brightness(0) flattens the whole PNG to
   ink — mark and wordmark together — which is what this system wants anyway.
   One line, no markup, and it fixes every page that shows the lockup. */
body.v2-on .brand-logo,
body.v2-on .login-logo img,
body.v2-on .topbar .brand img { filter: brightness(0); }

/* ============================================================================
   SIGN IN / SIGN UP — the login, billing and password-reset overlays
   These sit OUTSIDE #app, so they are scoped to body.v2-on like the public
   pages. Converting them matters more than it looks: they are the FIRST thing
   anyone sees, and with v2 on they were a dark box on a paper page.
   ========================================================================= */
body.v2-on .login-overlay { background: #f4f2ed; }
body.v2-on .login-box {
  background: #efece5; border: 1px solid #16150f; border-radius: 0;
  padding: 40px 36px; box-shadow: none; backdrop-filter: none;
}
body.v2-on .login-logo img { width: 240px; }
body.v2-on .login-box p { color: #57534a; font-size: 13px; }
body.v2-on .login-box > div[style*="font-size:20px"] { color: #16150f !important; }
body.v2-on .login-input {
  background: #fbfaf7; border: 1px solid #16150f; border-radius: 0;
  color: #16150f; font-family: 'Space Grotesk', sans-serif; text-align: left;
}
body.v2-on .login-input:focus { border-color: #246c80; outline: 2px solid rgba(36,108,128,0.25); outline-offset: -2px; }
body.v2-on .login-input::placeholder { color: #a09a8c; }
/* The one filled control on the screen. */
body.v2-on .login-btn {
  background: #16150f; border: 1px solid #16150f; border-radius: 0;
  color: #f4f2ed; font-family: 'Space Grotesk', sans-serif; font-weight: 500;
}
body.v2-on .login-btn:hover { background: #246c80; border-color: #246c80; }
body.v2-on .login-error { color: #c8352a; }
body.v2-on .login-overlay a { color: #246c80 !important; }
/* Hardcoded rather than tokenised because it overrides an inline colour — so
   it does NOT follow the token and has to be moved by hand. Kept in step. */
body.v2-on .login-box div[style*="color:var(--text-muted)"] { color: #67635b !important; }

/* ============================================================================
   MEMBERS — my-account.html
   The customer's own page: loyalty first, then history. Sits on the shared
   primitives above; this is only what is particular to it.
   ========================================================================= */

/* ---- GREETING ------------------------------------------------------------ */
body.v2-on .greeting { gap: 14px; }
body.v2-on .avatar {
  background: var(--steel-light, #e9e6de) !important;
  border: 1px solid #cfcabd !important; color: #57534a !important;
  font-weight: 700;
}
body.v2-on .greeting-text .hi {
  font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.16em;
  text-transform: uppercase; color: #67635b;
}
body.v2-on .greeting-text .who { color: #16150f; letter-spacing: -0.01em; }
body.v2-on .greeting-text .contact { font-family: 'DM Mono', monospace; color: #57534a; }
body.v2-on .brand-name { color: #16150f; }
body.v2-on .brand-mark { border: 1px solid #16150f; background: #efece5; }

/* ---- THE INTERFACE CHOICE (Settings › Appearance) ------------------------
   Radius 0 and ink edges, with the live half an ink ground.
   `body.v2-on` rather than `#app.v2` because every modal sits outside #app —
   the same reason the MODALS section at the foot of this file uses it. That
   section now dresses the rest of the Settings modal, so this block is no
   longer a lone control on an unstyled sheet; it only states what the
   segmented pair needs beyond the shared form primitives. */
body.v2-on .ui-choice button {
  border-radius: 0; border-color: var(--ink, #16150f); color: #57534a;
}
body.v2-on .ui-choice button:hover { color: #16150f; }
body.v2-on #uiChoiceLight {
  background: #16150f; color: #f4f2ed; border-color: #16150f;
}

/* ---- THE LOYALTY CARD ----------------------------------------------------
   Paper, like every other card, with gold ONLY in the filled punches.

   It was an ink field first, and that was wrong for two reasons the handoff
   states outright. Separation in this system is "by 1px rules and paper tones"
   — no heavy fields — so a full-bleed black slab was the one inverted surface
   on a page of paper and read as borrowed from another system. And ink is
   specified for text, rules, active fills and action squares: small,
   structural things. A status display is none of those.

   Gold now reads as the signal because it is the only saturated thing on the
   page rather than because it sits on black, and each gold cell is literally
   one sharpening banked. Same treatment on my-team's season package. */
body.v2-on .pass.loyalty {
  background: #efece5 !important;
  border: 1px solid #16150f !important; border-top: 2px solid #16150f !important;
}
body.v2-on .loyalty .pass-kicker {
  background: none !important; border: 1px solid #16150f !important; color: #16150f !important;
}
body.v2-on .loyalty .k-dot { background: var(--gold); }
/* The kicker's mono caps sit at 10px, and an emoji rendered at that size stops
   being a ticket and becomes a small coloured square. It needs its own size,
   which means its own element — hence the .k-ico wrapper in the markup. The
   line-height keeps the taller glyph from pushing the chip open. */
body.v2-on .pass-kicker .k-ico {
  font-size: 16px; line-height: 1; margin-right: 2px;
  position: relative; top: 1px;
}
body.v2-on .loyalty-pill {
  background: none; border: 1px solid #2f6b45; color: #2f6b45;
  font-family: 'DM Mono', monospace; letter-spacing: 0.12em;
}
/* Ink at full size. Solid, not a clipped gradient — background-clip:text paints
   the text transparent, so anywhere the gradient fails to render the balance
   simply is not there. */
body.v2-on .loyalty-num {
  background: none !important; -webkit-text-fill-color: #16150f !important;
  color: #16150f !important; letter-spacing: -0.03em;
}
body.v2-on .loyalty-of, body.v2-on .loyalty-label { color: #57534a; }
body.v2-on .loyalty-label b { color: #16150f; }
/* Each upright pill is one sharpening: outlined means used, gold means left. */
body.v2-on .track-dots { gap: 7px; }
body.v2-on .track-dots .punch-mark {
  background: none !important; border: 1px solid #aaa59a;
}
body.v2-on .track-dots .punch-mark.filled {
  background: var(--gold) !important; border-color: #9a7414;
  box-shadow: none;
}
body.v2-on .loyalty-foot { border-top: 1px solid #d8d4ca; }
body.v2-on .loyalty-saved { color: #57534a; }
body.v2-on .loyalty-saved b { color: #16150f; }
body.v2-on .loyalty-date { color: #67635b; }

/* ---- MINI CARDS + UPSELL + PRICING --------------------------------------- */
body.v2-on .mini-card {
  background: #efece5 !important; border: 1px solid #d8d4ca !important;
}
body.v2-on .mini-icon { background: #e9e6de !important; border: 1px solid #cfcabd; filter: grayscale(1); }
body.v2-on .mini-title { color: #16150f; }
body.v2-on .mini-sub { font-family: 'DM Mono', monospace; color: #67635b; }
body.v2-on .mini-count { color: #16150f; font-weight: 700; }

body.v2-on .upsell { background: #efece5 !important; border: 1px solid #16150f !important; }
body.v2-on .upsell .u-icon { filter: grayscale(1); }
body.v2-on .upsell h3 { color: #16150f; }
body.v2-on .upsell p { color: #57534a; }

body.v2-on .pricecard { background: #efece5 !important; border: 1px solid #16150f !important; }
body.v2-on .price-row { border-bottom: 1px solid #e2ded4; }
body.v2-on .price-name { color: #16150f; }
body.v2-on .price-name .sub { font-family: 'DM Mono', monospace; color: #67635b; }
body.v2-on .price-amt { color: #16150f; font-weight: 700; }
/* The highlighted tier is marked, not filled. A solid gold bar here was the
   heaviest gold on the page, which put an UPSELL above the customer's own
   balance on a loyalty-first screen. It gets an ink edge and a gold SAVE chip
   instead, so the punch card's gold cells stay the page's gold. */
body.v2-on .price-row.hl {
  background: #e9e6de !important;
  border: 1px solid #16150f !important; padding-left: 12px; padding-right: 12px;
}
body.v2-on .price-row.hl .price-name, body.v2-on .price-row.hl .price-amt { color: #16150f !important; }
body.v2-on .price-row.hl .sub { color: #57534a !important; }
body.v2-on .save-badge {
  background: var(--gold) !important; color: #16150f !important;
  font-family: 'DM Mono', monospace; letter-spacing: 0.1em;
}
body.v2-on .price-foot { color: #57534a; }
body.v2-on .price-foot b { color: #16150f; }

/* ---- HISTORY ------------------------------------------------------------- */
body.v2-on .hist { border-bottom: 1px solid #e2ded4; }
body.v2-on .hist-ic {
  background: #e9e6de !important; border: 1px solid #cfcabd !important;
  filter: grayscale(1);
}
/* The emoji is this chip's only content, so filtering the whole element is
   safe: grayscale then brightness(0) makes it an ink glyph on gold instead of
   a pink ticket. Other emoji on this page are greyscaled in place; the ones
   sitting inline with text can't be filtered without taking the text with
   them, so they keep their own colours for now. */
body.v2-on .hist-ic.gold {
  background: var(--gold) !important; border-color: var(--gold) !important;
  filter: grayscale(1) brightness(0);
}
body.v2-on .hist-main { color: #16150f; font-weight: 500; }
body.v2-on .hist-main .sk { color: #57534a; }
body.v2-on .hist-sub, body.v2-on .hist-date { color: #67635b; }
/* Chips, like every other chip in this system: 1px edge, no tinted fill. */
body.v2-on .badge {
  font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.1em;
  text-transform: uppercase; padding: 2px 6px;
  background: none !important; border: 1px solid #cfcabd; color: #57534a !important;
}
body.v2-on .badge-paid { border-color: #2f6b45; color: #2f6b45 !important; }
body.v2-on .badge-unpaid { border-color: #c8352a; color: #c8352a !important; }
body.v2-on .badge-bundle { border-color: #246c80; color: #246c80 !important; }
body.v2-on .badge-gold { border-color: #5f531c; color: #5f531c !important; background: var(--gold) !important; }

body.v2-on .show-more {
  background: none; border: 1px solid #16150f; color: #16150f;
  font-family: 'DM Mono', monospace; letter-spacing: 0.1em; text-transform: uppercase;
}
body.v2-on .show-more:hover { background: #e6e2d8; border-color: #16150f; color: #16150f; }
body.v2-on .no-data { color: #67635b; }
body.v2-on .add-skater {
  background: none !important; border: 1px solid #16150f !important; color: #16150f !important;
}
body.v2-on .add-skater:hover { background: #e6e2d8 !important; }

/* ---- EMPTY / ERROR STATES ------------------------------------------------ */
body.v2-on .state .ic { filter: grayscale(1); opacity: 0.6; }
body.v2-on .state h3 { color: #16150f; }
body.v2-on .state p { color: #57534a; }

/* ---- SHEETS -------------------------------------------------------------- */
body.v2-on .modal-overlay { background: rgba(22,21,15,0.45); }
body.v2-on .modal {
  background: #f4f2ed !important; border: 1px solid #16150f !important;
  border-radius: 0 !important;
}
body.v2-on .modal-handle { background: #cfcabd !important; }
body.v2-on .modal-title { color: #16150f; }
body.v2-on .form-select, body.v2-on .form-select option { color: #16150f; background: #fbfaf7; }
body.v2-on .modal-btn { border-radius: 0 !important; font-family: 'Space Grotesk', sans-serif; }
body.v2-on .modal-btn-primary { background: #16150f !important; color: #f4f2ed !important; border: 1px solid #16150f; }
body.v2-on .modal-btn-ghost { background: none !important; color: #16150f !important; border: 1px solid #16150f; }
body.v2-on .hollow-tag {
  background: none !important; border: 1px solid #cfcabd !important; color: #57534a !important;
  font-family: 'DM Mono', monospace;
}
body.v2-on .hollow-tag.selected {
  background: #16150f !important; border-color: #16150f !important; color: #f4f2ed !important;
}
body.v2-on .req-skater-card { background: #efece5 !important; border: 1px solid #d8d4ca !important; }
body.v2-on .req-skater-title { color: #16150f; }
body.v2-on .btn-add-skater-req { background: none !important; border: 1px solid #16150f !important; color: #16150f !important; }

/* ============================================================================
   TEAMS — my-team.html
   The coach's page. Same primitives again; this is the season package, the
   roster and the at-a-glance stats.
   ========================================================================= */

/* ---- SEASON PACKAGE -----------------------------------------------------
   Three states, and the ground follows the state rather than the component:
   FUNDED is the team's own punch card, so it takes the ink-and-gold treatment
   my-account's card established. The empty and absent states are neutral paper
   — an upsell should not look like money you already have. */
body.v2-on .pass.loyalty .pkg-bar {
  height: 12px; background: none;
  border: 1px solid #cfcabd; border-radius: 0 !important;
}
body.v2-on .pkg-bar-fill {
  background: var(--gold) !important; border-radius: 0 !important;
}
body.v2-on .pkg-bar-cap { font-family: 'DM Mono', monospace; color: #57534a; }
body.v2-on .pkg-bar-cap b { color: #16150f; }
/* The neutral/upsell variants sit on paper, so their bar has to invert. */
body.v2-on .pass:not(.loyalty) .pkg-bar { border-color: #cfcabd; }
body.v2-on .pass:not(.loyalty) .pkg-bar-cap { color: #57534a; }
body.v2-on .pass:not(.loyalty) .pkg-bar-cap b { color: #16150f; }

/* ---- ROSTER -------------------------------------------------------------- */
body.v2-on .skater { border-bottom: 1px solid #e2ded4; }
body.v2-on .skater-badge {
  background: #e9e6de !important; border: 1px solid #cfcabd !important;
  color: #57534a !important; border-radius: 0 !important;
}
body.v2-on .skater-name { color: #16150f; font-weight: 500; }
body.v2-on .skater-meta { font-family: 'DM Mono', monospace; color: #67635b; }
body.v2-on .skater-meta b { color: #16150f; }
body.v2-on .skater-meta .hollow { color: #16150f; font-weight: 700; }
body.v2-on .skater-meta .pos { color: #57534a; }
/* Goalie keeps a colour of its own — it changes which punch-card family the
   skate bills against, so it is information, not decoration. */
body.v2-on .skater-meta .pos.goalie { color: #5f531c; }
body.v2-on .sl-k, body.v2-on .sharp-lbl {
  font-family: 'DM Mono', monospace; letter-spacing: 0.12em; color: #67635b;
}
/* Counts are ink boxes on paper — the same square the operator's board uses
   for a row action, minus the glow the dark theme gave them. */
body.v2-on .sl-v {
  background: none !important; border: 1px solid #16150f !important;
  color: #16150f !important; border-radius: 0 !important;
}
body.v2-on .sl-v.none {
  border: 1px dashed #cfcabd !important; color: #67635b !important; font-style: normal;
}
body.v2-on .sharp-box {
  border: 1px solid #16150f !important; color: #16150f !important;
  border-radius: 0 !important;
}
body.v2-on .sharp-box.none {
  border: 1px dashed #cfcabd !important; color: #67635b !important;
}

/* ---- AT A GLANCE --------------------------------------------------------- */
body.v2-on .stats { gap: 1px; background: #d8d4ca; }
/* A 1px grid, not three floating cards — the same join the operator board uses
   between its status columns. */
body.v2-on .stat {
  background: #efece5 !important; border: none !important;
  border-radius: 0 !important; padding: 15px 12px;
}
body.v2-on .stat-num { color: #16150f; letter-spacing: -0.02em; }
body.v2-on .stat-num.teal { color: #16150f; }
body.v2-on .stat-num.gold { color: #5f531c; }
body.v2-on .stat-lbl {
  font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.12em;
  text-transform: uppercase; color: #67635b;
}

/* ---- QUEUE CHIPS --------------------------------------------------------- */
body.v2-on .badge-ready { border-color: #2f6b45 !important; color: #2f6b45 !important; }
body.v2-on .badge-waiting { border-color: #5f531c !important; color: #5f531c !important; }

/* ---- new-team.html — the team-side intake -------------------------------
   Shares everything above; only the stepper and the hollow pills are its own.
   (new-customer.html uses the same two, so these land there as well.) */
body.v2-on .stepper { border: 1px solid #16150f; background: none; }
body.v2-on .stepper button {
  background: none !important; border: none !important; color: #16150f !important;
  font-family: 'Space Grotesk', sans-serif;
}
body.v2-on .stepper button:hover { background: #e6e2d8 !important; }
body.v2-on .stepper .count { color: #16150f; border-left: 1px solid #d8d4ca; border-right: 1px solid #d8d4ca; }
body.v2-on .stepper .count span { color: #67635b; font-family: 'DM Mono', monospace; }
body.v2-on .pill {
  background: none !important; border: 1px solid #cfcabd !important; color: #57534a !important;
  font-family: 'DM Mono', monospace;
}
body.v2-on .pill.selected {
  background: #16150f !important; border-color: #16150f !important; color: #f4f2ed !important;
}
/* An explanatory note beside the pills. Its own literal rgba tint, so token
   remapping never reached it — paper-2 with an ink edge instead, the same
   treatment the wheel-side extras block uses for "attached to the thing above". */
body.v2-on .hollow-note {
  background: #efece5 !important; border-left: 2px solid #16150f !important;
  color: #57534a;
}
body.v2-on .hollow-note:empty { display: none; }
body.v2-on .hollow-note b { color: #16150f; }
body.v2-on .perk-main { color: #16150f; }
body.v2-on .perk-sub { color: #57534a; }
body.v2-on .submit-sub { color: #67635b; }
body.v2-on .skate-ico { filter: grayscale(1); }
body.v2-on .welcome-lockup { color: inherit; }

/* ============================================================================
   INTAKE LINK — new-customer.html
   The customer's own phone, standing at the counter. A stepped form, so the
   progress indicator and the step nav are the parts that are its own; the
   stepper and hollow pills already came with new-team.
   ========================================================================= */

/* ---- STEP PROGRESS ------------------------------------------------------
   Cells on a rule, filled as you go — the same reading as the punch card,
   which is the other "how far along am I" object in this system. These keep
   their `.dot` name; the punch card uses `.punch-mark`, so the two cannot
   inherit each other's shape or fill. */
body.v2-on .progress { gap: 5px; }
body.v2-on .progress .dot {
  width: 22px; height: 6px; border-radius: 0 !important;
  background: none !important; border: 1px solid #cfcabd;
  box-shadow: none !important; transition: none;
}
body.v2-on .progress .dot.done { background: #cfcabd !important; border-color: #cfcabd; opacity: 1; }
body.v2-on .progress .dot.active { background: #16150f !important; border-color: #16150f; width: 22px; }
body.v2-on .step-label {
  font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.18em;
  text-transform: uppercase; color: #67635b;
}

/* ---- STEP NAV -----------------------------------------------------------
   Continue is the one filled control on the screen; Back is an outline beside
   it. The accent glow under Continue goes with every other shadow. */
body.v2-on .btn-nav { border-radius: 0 !important; font-family: 'Space Grotesk', sans-serif; font-weight: 500; }
body.v2-on .btn-continue {
  background: #16150f !important; border: 1px solid #16150f !important;
  color: #f4f2ed !important; box-shadow: none !important;
}
body.v2-on .btn-continue:hover { background: #246c80 !important; border-color: #246c80 !important; }
body.v2-on .btn-continue:disabled { opacity: 0.35; }
body.v2-on .btn-back {
  background: none !important; border: 1px solid #16150f !important; color: #16150f !important;
}
body.v2-on .btn-back:hover { background: #e6e2d8 !important; }

/* ---- SKATERS + BLADES ---------------------------------------------------- */
body.v2-on .skater-card { background: #efece5 !important; border: 1px solid #d8d4ca !important; }
body.v2-on .skater-card-title { color: #16150f; }
body.v2-on .skater-remove, body.v2-on .blade-rm {
  background: none !important; border: 1px solid #cfcabd !important; color: #67635b !important;
}
body.v2-on .skater-remove:hover, body.v2-on .blade-rm:hover { border-color: #c8352a !important; color: #c8352a !important; }
body.v2-on .blade-row .blade-num {
  font-family: 'DM Mono', monospace; color: #67635b;
  background: none !important; border: 1px solid #cfcabd !important;
}
body.v2-on .add-btn {
  background: none !important; border: 1px dashed #16150f !important; color: #16150f !important;
  font-family: 'Space Grotesk', sans-serif;
}
body.v2-on .add-btn:hover { background: #e6e2d8 !important; border-style: solid !important; }
body.v2-on .summary-title { color: #16150f; }
body.v2-on .skater-chip {
  background: #e9e6de !important; border: 1px solid #cfcabd !important; color: #16150f !important;
  font-family: 'DM Mono', monospace; font-size: 11px; letter-spacing: 0.06em;
}
body.v2-on .skater-chip .mt { color: #67635b; }
body.v2-on .required { color: #c8352a; }
body.v2-on .contact-hint { color: #67635b; }
body.v2-on .pay-note { color: #57534a; }
body.v2-on .pay-note strong { color: #16150f; }

/* ---- PRICE ROWS ---------------------------------------------------------
   Same treatment as my-account's pricing: the highlighted row is the offer, so
   it takes the one gold fill and everything else stays paper. */
body.v2-on .pr-row { border-bottom: 1px solid #e2ded4; }
body.v2-on .pr-name { color: #16150f; }
body.v2-on .pr-sub { font-family: 'DM Mono', monospace; color: #67635b; }
body.v2-on .pr-price { color: #16150f; font-weight: 700; }
/* Same object as my-account's highlighted tier, same treatment — marked with
   an ink edge and a gold chip rather than filled. */
body.v2-on .pr-row.gold {
  background: #e9e6de !important; border: 1px solid #16150f !important;
}
body.v2-on .pr-row.gold .pr-name, body.v2-on .pr-row.gold .pr-price { color: #16150f !important; }
body.v2-on .pr-row.gold .pr-sub { color: #57534a !important; }
body.v2-on .pr-save {
  background: var(--gold) !important; color: #16150f !important;
  font-family: 'DM Mono', monospace; letter-spacing: 0.1em;
}

/* ---- DONE ---------------------------------------------------------------- */
body.v2-on .success-icon { filter: grayscale(1); }
body.v2-on .success-screen h2 { color: #16150f; letter-spacing: -0.02em; }
body.v2-on .success-screen p { color: #57534a; }

/* Selects that carry a "filled" state alongside .form-input. */
body.v2-on .form-select.filled { color: #16150f; }

/* ============================================================================
   TEAMS + REPORTS — the last two operator panes
   Not in the Aug 31 handoff, which covers Orders and Customers only. Extended
   from the same system: rows on rules, 1px joins, ink for structure, gold
   nowhere but Scan.
   ========================================================================= */

@media (pointer: coarse) and (hover: none) and (orientation: landscape),
       (hover: hover) and (min-width: 1000px) {

  /* ---- TEAMS ------------------------------------------------------------
     Same shape as the customer roster: a list that scrolls against a fixed
     record panel, meeting on a 1px rule. #teamLayout has no A–Z rail, so it is
     two columns rather than three. */
  #app.v2 #teamLayout { grid-template-columns: minmax(0, 1fr) 386px; gap: 0; }
  /* Rows size to their CONTENT. `.customer-grid` is height:100% so the list can
     scroll, and a grid with two children and no auto-rows splits that height
     between them — which is why two teams rendered as two half-screen slabs. */
  #app.v2 .customer-grid { grid-auto-rows: min-content; align-content: start; }
  #app.v2 .team-card {
    background: none; border: none; border-bottom: 1px solid var(--line-soft);
    border-left: 3px solid transparent;
    padding: 15px 22px; backdrop-filter: none; transform: none;
  }
  #app.v2 .team-card:hover { background: #eeebe3; transform: none; }
  #app.v2 .team-card.active { background: var(--paper-2); border-left-color: var(--ink); }
  #app.v2 .team-name { font-size: 15px; font-weight: 500; color: var(--ink); }
  #app.v2 .team-meta {
    font-family: 'DM Mono', monospace; font-size: 11px; color: var(--text-muted);
    text-transform: none; letter-spacing: 0;
  }
  /* The player count is inline-styled `color: var(--accent)` in teamCardHtml,
     so only an attribute match reaches it. Ink, like the roster's visit count —
     a count is not a link.
     The `~15 · roster pending` variant keeps its gold: that one is flagging an
     incomplete team, which is information, the same reason goalie kept a
     colour of its own on my-team. */
  #app.v2 .team-card div[style*="var(--accent)"] { color: var(--ink) !important; }
  /* Archived teams: a band, like the roster's letter headers. */
  #app.v2 #teamsView details > summary {
    background: var(--paper-3); border-bottom: 1px solid var(--line);
    padding: 9px 22px; font-family: 'DM Mono', monospace; font-size: 10px;
    letter-spacing: 0.18em; text-transform: uppercase; color: #6b675e;
  }
  #app.v2 #teamRegistrationsBar {
    margin: 0; padding: 11px 24px; border-radius: 0;
    background: #f0eee6; border: none; border-bottom: 1px solid var(--line);
  }

  /* ---- REPORTS ----------------------------------------------------------
     Almost all of Reports is inline styles built from tokens, so the palette
     already carried. What it did NOT carry is shape: everything was a rounded
     card on its own island. These are the rules that turn the islands into a
     ruled sheet. */
  #app.v2 #reportsView { padding: 0 24px 24px; }
  /* The period tabs are the same segmented control as everywhere else. */
  #app.v2 #reportPeriodTabs {
    gap: 0; margin: 14px 0 18px; border: 1px solid var(--ink); width: fit-content;
  }
  #app.v2 .rpt-tab {
    background: none; border: none; border-right: 1px solid var(--ink);
    padding: 8px 13px; color: var(--text-secondary);
    font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.12em;
    text-transform: uppercase;
  }
  #app.v2 .rpt-tab:last-child { border-right: none; }
  #app.v2 .rpt-tab:hover { background: var(--v2-hover); color: var(--ink); }
  #app.v2 .rpt-tab.btn-primary, #app.v2 .rpt-tab.active {
    background: var(--ink) !important; color: var(--paper) !important;
  }
  /* Stat cards become one grid joined by 1px rules — the same join the order
     board uses between its status columns, and my-team's At a Glance. */
  #app.v2 #reportCards { gap: 1px !important; background: var(--line); }
  #app.v2 #reportCards > div {
    background: var(--paper-2) !important; border: none !important;
  }
  /* The section panels stay separate objects, but flat and square. */
  #app.v2 #reportSections > div,
  #app.v2 #reportSections > div > div {
    border-color: var(--line) !important;
  }
  #app.v2 #reportsView [style*="background:var(--surface)"],
  #app.v2 #reportsView [style*="background: var(--surface)"] {
    background: var(--paper-2) !important; border-color: var(--line) !important;
  }
  /* The little distribution bars: a track with a 1px edge and an ink fill,
     rather than a rounded pill in the accent. */
  #app.v2 #reportSections [style*="height:6px"] {
    border: 1px solid var(--line-chip); background: none !important; height: 8px !important;
  }
  #app.v2 #reportSections [style*="height:100%"][style*="background:var(--accent)"] {
    background: var(--ink) !important;
  }
  /* Money is the SUBJECT of this screen, so colouring every dollar figure gold
     spends the signal colour six times on one page and it stops signalling
     anything. Ink for the figures; red stays on what is owed, because that is
     the one number the shop has to act on. Inline styles, so this has to match
     on the attribute. */
  #app.v2 #reportCards div[style*="var(--gold)"],
  #app.v2 #reportSections div[style*="var(--gold)"] { color: var(--ink) !important; }
}

/* ============================================================================
   MODALS — where the work actually happens
   Twenty of them: new order, pickup, scanner, settings, intake, QR, and the
   rest. They sit OUTSIDE #app in the markup, so `#app.v2` never reached them —
   these are scoped to `body.v2-on` like the public pages, which also means the
   form primitives written for those pages already apply here. Same controls,
   same look, one set of rules.
   ========================================================================= */

/* Ink at 45%, no blur. The blur was doing the job a hard edge does in this
   system: the modal has a 1px ink border and the board behind it is paper, so
   the boundary is already unambiguous. */
body.v2-on .modal-overlay {
  background: rgba(22,21,15,0.45); backdrop-filter: none !important;
}
body.v2-on .modal {
  background: #f4f2ed; border: 1px solid #16150f; border-radius: 0 !important;
  box-shadow: none !important;
}
/* 2px under the header — the same weight the rail brand and the customer
   record use. It says "this is a thing, not a row". */
body.v2-on .modal-header {
  padding: 18px 22px; border-bottom: 2px solid #16150f; background: #efece5;
}
body.v2-on .modal-title { font-size: 18px; font-weight: 700; color: #16150f; letter-spacing: -0.01em; }
body.v2-on .modal-body { padding: 20px 22px; background: #f4f2ed; }
body.v2-on .modal-footer {
  padding: 14px 22px; border-top: 1px solid #d8d4ca; background: #efece5;
}

/* The operator app's own form primitives (index.html's, not the public pages')
   land here too — same class names, so the rules written for beta.html cover
   them. These are the few index-only additions. */
body.v2-on .form-select { background: #fbfaf7; border: 1px solid #16150f; color: #16150f; }
body.v2-on .form-select option { background: #f4f2ed; color: #16150f; }
body.v2-on .form-group { margin-bottom: 16px; }

/* Hollow tags: the same square, outlined-then-filled control as the pills on
   the intake pages. */
body.v2-on .hollow-tag {
  background: none !important; border: 1px solid #cfcabd !important;
  color: #57534a !important; border-radius: 0 !important;
  font-family: 'DM Mono', monospace;
}
body.v2-on .hollow-tag:hover { border-color: #16150f !important; color: #16150f !important; }
body.v2-on .hollow-tag.selected {
  background: #16150f !important; border-color: #16150f !important; color: #f4f2ed !important;
}

/* Buttons inside modals. One filled control per footer; everything else is an
   outline. Gold appears nowhere — it is the Scan surface and nothing else. */
body.v2-on .modal .btn, body.v2-on .modal-footer .btn {
  border-radius: 0 !important; font-family: 'Space Grotesk', sans-serif; font-weight: 500;
}
body.v2-on .modal .btn-primary {
  background: #16150f !important; border: 1px solid #16150f !important; color: #f4f2ed !important;
}
body.v2-on .modal .btn-primary:hover { background: #246c80 !important; border-color: #246c80 !important; }
body.v2-on .modal .btn-ghost {
  background: none !important; border: 1px solid #16150f !important; color: #16150f !important;
}
body.v2-on .modal .btn-ghost:hover { background: #e6e2d8 !important; }
body.v2-on .modal .btn-danger {
  background: none !important; border: 1px solid #c8352a !important; color: #c8352a !important;
}
body.v2-on .modal .btn-danger:hover { background: #c8352a !important; color: #f4f2ed !important; }

/* Extra-skater blocks inside the intake and order modals. */
body.v2-on .modal-skater-card {
  background: #efece5 !important; border: 1px solid #d8d4ca !important;
}
body.v2-on .modal-skater-title { color: #16150f; }
body.v2-on .modal-skater-remove {
  background: none !important; border: 1px solid #cfcabd !important; color: #67635b !important;
}
body.v2-on .modal-skater-remove:hover { border-color: #c8352a !important; color: #c8352a !important; }

/* The quick-add roster rows in the team modal. */
body.v2-on .qa-row .form-input, body.v2-on .qa-row .form-select { padding: 8px 10px; }

/* The two JS-painted segmented toggles — Fulfilment (queue / done now) and the
   +Visit kind (sharpening / service / goods). They paint from these tokens now,
   so they take the same ink-fill-with-paper-text every other active state in
   this system uses. */
body.v2-on { --seg-on-bg: #16150f; --seg-on-fg: #f4f2ed; }
/* Their track is inline-styled too, so it needs an attribute match. */
body.v2-on .modal div[style*="background:var(--surface)"] {
  background: none !important; border-color: #16150f !important;
}

/* Native controls follow the ink. */
body.v2-on input[type="checkbox"], body.v2-on input[type="radio"] { accent-color: #16150f; }
body.v2-on input[type="date"], body.v2-on input[type="time"] { color-scheme: light; }

/* Radius 0 inside modals. The blanket reset is scoped to `#app.v2 *`, and
   modals live outside #app — which is why the pickup sheet's price fields were
   still pills on a square sheet. */
body.v2-on .modal * { border-radius: 0 !important; }

/* The checkbox accent is written inline as `accent-color: var(--accent)`, so it
   follows the remapped teal rather than ink. Attribute match to reach it. */
body.v2-on input[style*="accent-color"] { accent-color: #16150f !important; }

/* PAYMENT CHIPS (pickup sheet). Outlined until chosen, then filled ink — the
   same active state as every other control in this system. Unpaid keeps red,
   because it is the one choice with a consequence the shop chases later. */
body.v2-on .pay-chip {
  background: none !important; border: 1px solid #cfcabd !important; color: #57534a !important;
}
body.v2-on .pay-chip:hover { border-color: #16150f !important; color: #16150f !important; }
body.v2-on .pay-chip.active {
  background: #16150f !important; border-color: #16150f !important; color: #f4f2ed !important;
}
body.v2-on .pay-chip.unpaid.active {
  background: #c8352a !important; border-color: #c8352a !important; color: #f4f2ed !important;
}
/* The brand logos inside the chips are colour images; flatten them so a chip
   reads as one object rather than a label with a sticker on it. */
body.v2-on .pay-chip-ico { filter: grayscale(1); }
body.v2-on .pay-chip.active .pay-chip-ico { filter: grayscale(1) invert(1) brightness(1.6); }

/* ============================================================================
   ADMIN — the cross-shop pane (super-admin only)
   Like Reports, it is built almost entirely from inline styles using tokens the
   palette already remapped, so the colour carried for free. What it needed was
   STRUCTURE: four panels — waitlist, beta codes, sticker batches, shops — that
   ran together with nothing marking where one ended.
   ========================================================================= */

@media (pointer: coarse) and (hover: none) and (orientation: landscape),
       (hover: hover) and (min-width: 1000px) {

  #app.v2 #adminView { padding: 14px 24px 24px; }

  /* Each heading shares a flex row with its own buttons, so it cannot be a
     full-width band the way the roster's letter headers are — it would have to
     sit on top of the actions rather than beside them. A rule BETWEEN panels
     does the same job: it marks where one list ends, which is the thing that
     was missing. The heading stays a mono label.
     Panels have inline margin-bottom, hence the !important. */
  #app.v2 #adminView h3 {
    font-family: 'DM Mono', monospace; font-size: 10px; font-weight: 400;
    letter-spacing: 0.18em; text-transform: uppercase; color: #6b675e;
  }
  #app.v2 #adminView > div {
    margin: 0 0 20px !important; padding-top: 18px;
    border-top: 1px solid var(--line);
  }
  #app.v2 #adminView > div:first-child { padding-top: 0; border-top: none; }
  #app.v2 #adminView > div > div:first-child { align-items: center; }

  /* Rows: a ruled list, not a stack of cards with gaps. */
  #app.v2 #adminList, #app.v2 #waitlistList,
  #app.v2 #betaCodesList, #app.v2 #stickerBatchesList { gap: 0 !important; }
  #app.v2 #adminList > div, #app.v2 #waitlistList > div,
  #app.v2 #betaCodesList > div, #app.v2 #stickerBatchesList > div {
    background: none !important; border: none !important;
    border-bottom: 1px solid var(--line-soft) !important;
    padding: 13px 0 !important;
  }
  #app.v2 #adminList > div:hover, #app.v2 #waitlistList > div:hover { background: #eeebe3 !important; }

  /* The plan/status toggle takes the segmented treatment. */
  #app.v2 .admin-pos-toggle { border: 1px solid var(--ink); }
  #app.v2 .admin-pos-btn { color: var(--text-secondary); font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; }
  #app.v2 .admin-pos-btn:first-child { border-right: 1px solid var(--ink); }
  #app.v2 .admin-pos-btn.active { background: var(--ink) !important; color: var(--paper) !important; }

  /* Admin is the one pane that talks to other shops' money, so its errors are
     load-bearing — give them the same red-edged block the app uses elsewhere
     rather than a loose line of red text. */
  #app.v2 #adminView div[style*="var(--danger)"] {
    border-left: 2px solid var(--danger); padding-left: 10px;
  }
}

/* ---- PHONE LANDSCAPE: the other panes ------------------------------------
   Customers and Teams are reachable from the rail here, so they have to be
   usable at 844x390 — but the roster rules written for the tablet live inside
   the rail media query, which this layout never enters. Without these, tapping
   the Customers chip landed on a pane with half-size columns, wrapped names and
   a dashed empty panel: the switch worked and the destination was broken.
   Compact versions of the same objects. */
#app.v2.phone-land #customersView .toolbar-filters { margin-left: auto; display: flex; gap: 0; border: 1px solid var(--ink); flex-wrap: nowrap; }
#app.v2.phone-land #customersView .toolbar-filters .btn {
  background: none; border: none; border-right: 1px solid var(--ink);
  padding: 5px 9px; color: var(--text-secondary);
  font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.1em;
  text-transform: uppercase; white-space: nowrap;
}
#app.v2.phone-land #customersView .toolbar-filters .btn:last-child { border-right: none; }
#app.v2.phone-land #customersView .toolbar-filters .btn.btn-primary { background: var(--ink) !important; color: var(--paper) !important; }
#app.v2.phone-land .pane-title, #app.v2.phone-land .pane-count, #app.v2.phone-land .pane-link { display: none; }

/* Two columns, no A–Z rail — 26px of letters is not worth it against a record
   panel on a screen this short. */
#app.v2.phone-land #custLayout,
#app.v2.phone-land #custLayout.rail-off { grid-template-columns: minmax(0, 1fr) 300px; gap: 0; }
#app.v2.phone-land #teamLayout { grid-template-columns: minmax(0, 1fr) 300px; gap: 0; }
/* Once a record is OPEN the slack changes hands. Browsing, the list should be
   wide and the record is a placeholder; reading a record, the list is only a
   way to switch back and the record is the work.
   Measured at 844x390: the record wanted 335px and had 300, so it scrolled
   sideways — while the list held 343px for rows needing 292. Fixing the list at
   280 and letting the record take the rest gives it 363, which clears its
   content and ends the sideways scroll. */
#app.v2.phone-land #custLayout.detail-open,
#app.v2.phone-land #teamLayout.detail-open { grid-template-columns: 280px minmax(0, 1fr); }
#app.v2.phone-land .az-rail { display: none; }
#app.v2.phone-land .cust-layout { align-items: stretch; }
#app.v2.phone-land .customer-grid {
  background: var(--paper); padding: 0; gap: 0; max-height: none; height: 100%;
  overflow-y: auto; min-height: 0; grid-auto-rows: min-content; align-content: start;
}
#app.v2.phone-land .detail-col {
  position: static; background: var(--paper-2); height: 100%; overflow-y: auto; min-height: 0;
}

/* Rows, tightened for the short screen. */
#app.v2.phone-land .customer-card,
#app.v2.phone-land .team-card {
  grid-template-columns: 32px minmax(0,1fr) auto; gap: 10px;
  padding: 9px 14px; margin: 0;
  background: none; border: none; border-bottom: 1px solid var(--line-soft);
  border-left: 3px solid transparent; backdrop-filter: none; transform: none;
}
#app.v2.phone-land .customer-card:hover,
#app.v2.phone-land .team-card:hover { background: #eeebe3; transform: none; }
#app.v2.phone-land .customer-card.active,
#app.v2.phone-land .team-card.active { background: var(--paper-2); border-left-color: var(--ink); }
#app.v2.phone-land .avatar {
  width: 32px; height: 32px; font-size: 11px; font-weight: 700;
  background: var(--line-soft); border: 1px solid var(--line-chip); color: var(--text-secondary);
}
#app.v2.phone-land .customer-name, #app.v2.phone-land .team-name { font-size: 14px; font-weight: 500; color: var(--ink); }
#app.v2.phone-land .customer-meta, #app.v2.phone-land .team-meta {
  font-family: 'DM Mono', monospace; font-size: 10px; color: var(--text-muted);
  text-transform: none; letter-spacing: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#app.v2.phone-land .visit-count { font-size: 15px; font-weight: 700; color: var(--ink); }
#app.v2.phone-land .visit-label, #app.v2.phone-land .balance-badge { font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; }
#app.v2.phone-land .balance-badge { background: none; border: 1px solid var(--line-chip); color: var(--text-secondary); padding: 1px 5px; }
#app.v2.phone-land .balance-owed { border-color: var(--danger); color: var(--danger); }
/* The quick-sharpen square goes: at this width the row is already three columns
   and the action belongs on the record, not on every line. */
#app.v2.phone-land .quick-sharpen-btn { display: none; }
/* The team player count is inline-styled `color: var(--accent)`. The ink
   override lives in the rail media query, which this layout never enters, so
   the count stayed teal here while it is ink everywhere else. */
#app.v2.phone-land .team-card div[style*="var(--accent)"] { color: var(--ink) !important; }
#app.v2.phone-land .letter-header, #app.v2.phone-land .list-section-title {
  background: var(--paper-3); border-bottom: 1px solid var(--line);
  padding: 5px 14px; font-size: 9px; letter-spacing: 0.18em; color: #6b675e;
}
#app.v2.phone-land .detail-empty { border: none; background: none; padding: 30px 16px; font-size: 12px; }

/* ---- DUE CHIPS -----------------------------------------------------------
   neededByInfo writes its colour inline, which is right for the dark theme and
   wrong on paper: gold text measures 1.47:1 against #f4f2ed — below the 3:1
   floor at any size. The tone class is the hook; these are the paper values.
   Outlined rather than filled in every tier, so the ⏰ keeps a light ground
   (it renders in its own colours and would fight a saturated fill), and so at
   most one thing per row is ever a solid block. */
body.v2-on .due-badge {
  font-family: 'DM Mono', monospace; font-size: 9px; font-weight: 400;
  letter-spacing: 0.08em; text-transform: uppercase; padding: 2px 6px;
}
/* FILLED, not outlined. Gold as TEXT is 1.47:1 and unreadable; gold-ink is
   readable but reads as a dark outline rather than as gold — Dave's call, and
   he is right, #6b5d1f is an olive. Gold as a GROUND with ink on it is 11.14:1
   and unmistakably gold, which is also how the handoff draws these (7a gives
   the chip a dueBg, not just a dueFg).
   Filling `soon` forces `overdue` to fill too, or the calmer tier would be the
   louder object. So the escalation is: red fill → gold fill → plain outline. */
body.v2-on .due-overdue, body.v2-on .due-today {
  background: #c8352a !important; border-color: #c8352a !important; color: #f4f2ed !important;
}
body.v2-on .due-soon {
  background: #e8c84a !important; border-color: #e8c84a !important; color: #16150f !important;
}
/* The far tier stays quiet — it is information, not a flag. */
body.v2-on .due-later {
  background: none !important; color: #57534a !important; border-color: #cfcabd !important;
}
/* The clock inherits the chip's ink now, so each chip reads as one object.
   ⏰ rendered in its own pink-red on every tier — the one colour that belonged
   to none of them. */
body.v2-on .due-badge { display: inline-flex !important; align-items: center; gap: 5px; }

/* Base size for the due clock: the SVG is in shared markup, so the shipped
   theme needs it too or an unsized <svg> defaults to 300x150. */
.due-ico { width: 11px; height: 11px; display: inline-block; vertical-align: -1px; flex: none; }

/* The one-tap queue action. A plus rather than a bolt — it adds a job to the
   queue, and the bolt was doing double duty against the per-row quick-sharpen.
   Gold on the ink button is 11.14:1, and it is the one place gold appears in
   the record panel, which is right: this is the panel's primary action.
   Base size is unscoped — the SVG is in shared markup, and an unsized <svg>
   defaults to 300x150. */
.queue-ico { width: 14px; height: 14px; display: inline-block; vertical-align: -2px; flex: none; }
body.v2-on .queue-ico { color: #e8c84a; }

/* ---- GOLD AS TEXT: the sweep ---------------------------------------------
   Dave flagged the revenue figure. It is one of seven places the app writes
   gold as a TEXT colour — correct on midnight blue, 1.47:1 on paper, which
   fails at any size including the 20px ones.

   Two rules, applied consistently:
     · gold as a GROUND with ink on it — fine, and how the punch card, the SAVE
       chip and the due chips now work.
     · gold as TEXT on paper — never. It becomes ink, or an ink-on-gold chip
       where the thing genuinely needs to flag itself.
   The dark theme keeps its literals; these are all v2-scoped. */

/* The revenue tile. Ink, like the three tiles beside it — they are one set, and
   the label already says REVENUE. Green was the alternative (money in, 6.4:1)
   but it would make one of four tiles shout. Reports made the same call. */
body.v2-on .stat-mini.money .sm-val { color: #16150f !important; }

/* "Punch Card" on a visit row, and in order history. */
body.v2-on .visit-hollow[style*="var(--gold)"],
body.v2-on #customerDetail span[style*="color:var(--gold)"],
body.v2-on .modal span[style*="color:var(--gold)"] { color: #16150f !important; }

/* Admin: the sticker-batch code chip and the in-stock badge. Both were gold on
   a near-paper ground; the batch chip keeps its edge, the badge becomes a
   proper gold chip since "in stock" is the thing it exists to flag. */
body.v2-on #adminView span[style*="color:var(--gold)"] {
  color: #5f531c !important; border-color: #5f531c !important;
}
body.v2-on #adminView .badge[style*="rgba(232,200,74"] {
  background: #e8c84a !important; color: #16150f !important;
}

/* The team card's "~15 skaters · roster pending". I had kept this gold on the
   grounds that it flags an incomplete roster — true, but I never checked it
   against paper, and at 1.47:1 it flagged nothing. Ink for the number; the
   words under it already say "roster pending". */
body.v2-on .team-card div[style*="color:var(--gold)"],
body.v2-on #teamDetailPanel div[style*="color:var(--gold)"] { color: #16150f !important; }

/* ---- CUSTOMER STAT TILES: value first --------------------------------------
   The four tiles staggered: "Total visits" and "Last visit" wrap to two lines
   while "Revenue" and "Skaters" do not, so the numbers started 14px apart and
   the row read as broken.

   Reordering rather than padding the labels out, because value-above-label is
   already THE pattern in this system — the rail foot and my-team's At a Glance
   both put the figure first with a mono caption under it. So the alignment
   comes free and the tile stops being the odd one out.

   `order` on the child rather than column-reverse: the DOM stays label-then-
   value, which is the right reading order for a screen reader ("Total visits,
   1"), and only the paint order changes. */
body.v2-on .stat-mini {
  display: flex; flex-direction: column; justify-content: flex-start; gap: 3px;
}
body.v2-on .stat-mini .sm-val { order: -1; line-height: 1.1; }
body.v2-on .stat-mini .sm-label {
  font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.12em;
  text-transform: uppercase; color: #67635b; line-height: 1.3;
}

/* ---- THE STAT FOLD --------------------------------------------------------
   Collapsed by default. These numbers are context, not instructions: the
   counter needs hollow, profile, boot and balance, and four tiles of loyalty
   trivia were pushing all of that down a panel that is 300px wide and scrolls.
   The summary keeps the two that can change a decision — how recent, how many
   skaters — in one quiet mono line. */
body.v2-on .stat-fold { margin-bottom: 16px; border-bottom: 1px solid #e2ded4; }
body.v2-on .stat-fold > summary {
  list-style: none; cursor: pointer; display: flex; align-items: center; gap: 8px;
  padding: 8px 0; font-family: 'DM Mono', monospace; font-size: 10px;
  letter-spacing: 0.1em; text-transform: uppercase; color: #67635b;
}
body.v2-on .stat-fold > summary::-webkit-details-marker { display: none; }
body.v2-on .stat-fold > summary:hover { color: #16150f; }
body.v2-on .sf-chev { display: inline-block; transition: transform 0.15s; font-size: 9px; }
body.v2-on .stat-fold[open] .sf-chev { transform: rotate(90deg); }
/* Open, the tiles keep the layout they already have — value above label, one
   shared top edge — just inside the fold. */
body.v2-on .stat-fold .stat-strip { margin: 4px 0 12px; }

/* ---- HISTORY ROWS ---------------------------------------------------------
   Four columns — date, label, price, status — in a 386px panel computed to
   90px / 64px / 47px / 86px, so the LABEL got 64px and "1/2" · Coronation Ace"
   wrapped onto three lines. Rows ran 86-106px tall for one line of content.
   Four columns is a table's shape, and this is not a table; it is a list of
   rows in a narrow panel.

   Two columns, two rows instead — what the panel can actually hold:

       label · skater · blade                     $15.00
       Aug 20, 2026                          [Paid · Cash] ✕

   The label gets the full width less the price, the date drops to the quiet
   second line where it belongs, and money and status stack on one right edge.
   Placement only — the DOM order stays date, label, price, actions, which is
   the sensible reading order. */
/* The two-sided lockup moved to index.html — it was right for both themes, not
   just this one. What stays here is the PAINT: a ruled row instead of a bordered
   card, and the tighter padding a 386px paper panel wants. */
#app.v2 .visit-row {
  background: none; border: none; border-bottom: 1px solid var(--line-soft);
  border-radius: 0 !important; padding: 10px 2px;
}
#app.v2 .visit-price { font-family: 'DM Mono', monospace; color: var(--ink); font-size: 13px; }
#app.v2 .visit-date { color: var(--text-secondary); }
#app.v2 .visit-hollow { font-size: 13px; font-weight: 700; color: var(--ink); }

/* Status chips join the system: mono, square, 1px edge, no tinted fill. */
#app.v2 .visit-paid {
  font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 0.08em;
  text-transform: uppercase; padding: 2px 6px; border-radius: 0 !important;
  background: none !important; border: 1px solid var(--line-chip);
  color: var(--text-secondary) !important;
}
#app.v2 .visit-paid.paid-yes { border-color: var(--success); color: var(--success) !important; }
#app.v2 .visit-paid.paid-no { border-color: var(--danger); color: var(--danger) !important; }
/* The ✕ is taken out of flow in index.html now, for both themes — see the
   .delete-visit-btn block there for why, and for the ::after that keeps its
   44x44 target. Only the two numbers that differ live here: this row runs at
   `padding: 10px 2px` in a 386px panel rather than the shipped 12px/16px, so
   it reserves a narrower column and seats the glyph tighter to the edge. */
#app.v2 .visit-row { padding-right: 28px; }
#app.v2 .delete-visit-btn { right: 0; color: var(--text-muted); }
#app.v2 .delete-visit-btn:hover { color: var(--danger); }
