/* ============================================================================
   portal.css — THE portal stylesheet (vendor portal now; customer portal phase 7
   adopts this file — portal-pattern rule: screens COMPOSE these classes and never
   declare their own styles).

   VISUAL DIRECTION (operator-approved mockup, 2026-08-14): a BUSINESS TOOL, the
   admin dashboard as the bar — dense, quiet, hairlines, small radii, 14px base,
   text-only navigation. 🔴 48px tap targets are a PHONE rule, not a font-size
   rule: --touch is 32px on desktop and 48px under 900px. Mobile-first physics
   stay; the admin's app.css is still never shared (different physics) — only
   its restraint is.
   ========================================================================== */
:root{
  --fs-0:14px; --fs-1:16px; --fs-2:20px;                     /* quiet scale */
  --sp-1:6px; --sp-2:10px; --sp-3:14px; --sp-4:20px; --sp-5:28px;
  --r:4px; --touch:32px;
  --ink:#1a1d21; --muted:#6b7280; --line:#e4e4e2; --bg:#f7f7f5; --card:#fff;
  --accent:#35598f; --accent-bg:#eaf1fa;   /* the accent, and a tint of it for a surface that must not read as page */
  /* 🔴 THE INK THAT SITS ON THE ACCENT, NAMED (2026-09-09, the filled tag chip). It was written inline as
     `color:#fff`, which is how this stylesheet's third blue got in — every finding here says the same thing:
     a colour with no name drifts. Naming it also makes the admin's copy of the chip byte-identical rather than
     "identical except one value", because that surface has the same token for the same job. */
  --accent-ink:#fff;
  /* CHIP-ON = the ticked photo-tag chip. Same blue as today (--accent), named so the admin can resolve its
     own affirmative colour without a second declaration. --chip-tick none = no checkmark on this surface. */
  --chip-on:#35598f; --chip-on-ink:#fff; --chip-tick:none;
  --go:#2f855a; --go-bg:#e9f3ee; --stop:#c0392b; --stop-bg:#faeae7;
  --warn:#b7791f; --warn-bg:#faf3e3; --off:#eceff1;
  /* 🔴 A STRONGER STEP FOR STATE BADGES (operator, 2026-09-02). The existing pairs do NOT support
     one: there is exactly one amber ink + one amber surface and no darker variant, so a badge could
     only be strengthened by inventing a colour inline — which is how .btn-assist's hardcoded
     #2b6cb0 became this page's third blue. These are NAMED TOKENS on the SAME hues, added beside
     their originals, and the originals are untouched for every other use.
     Chosen arithmetically, not by eye (WCAG relative luminance):
       Needs pricing  ink/bg 3.29:1 -> 6.22:1   ·  bg vs card white 1.11:1 -> 1.58:1
       Active brand   ink/bg 4.01:1 -> 6.34:1   ·  bg vs card white 1.13:1 -> 1.63:1
     A surface at 1.11:1 against a white card is barely a surface — that is why the badge did not
     register across the grid. The two are matched to within 0.12 and 0.05 so the states read with
     EQUAL weight, which is the other half of the ruling. */
  --warn-bg-strong:#ecca7a; --warn-ink:#5f3c07;
  --go-bg-strong:#aed3c0;   --go-ink:#17492f;
  /* 🔴 "Needs pricing" IS RED (operator, 2026-09-02): amber did not register against nine other
     cards, and the fleet caps at ~10 concepts — the other ~50 planned sites are GEO AGGREGATORS,
     not brands — so a vendor sees a handful of these at most, never a wall.
     ⚠️ --stop DOES NOT FIT AS THE INK, which is worth stating because reusing it was the first
     instruction: #c0392b maxes at 3.54:1 (below AA) on a surface only 1.54:1 against the card, so it
     cannot be both readable AND sit on a surface strong enough to see. Same shape as the amber
     finding: the palette has one red ink and one pale red surface, and no darker step. These are
     named tokens on the SAME hue; --stop itself is untouched and still carries "Leave this brand".
     Matched to the green so the two states read with EQUAL weight:
       Needs pricing  ink/bg 6.78:1  ·  surface vs card 1.63:1
       Active brand   ink/bg 6.34:1  ·  surface vs card 1.63:1   (deltas 0.44 and 0.00) */
  --stop-bg-strong:#f1bfb9; --stop-ink:#75190f;
  /* 🔴 STREET-LIGHT YELLOW FOR A SIGNAL BAR (operator, 2026-09-08 round 5): "I don't want amber for
     expiring, I want street light yellow — amber is hard to tell and I could go either red or yellow."
     ⚠️ --warn CANNOT BE THE BAR, and the reason is measurable rather than aesthetic. #b7791f sits at
     L* 55.9 against --stop's L* 44.7 — ELEVEN lightness points apart — and its hue is ALREADY 72.7°,
     i.e. yellow. So the confusion was never hue: two dark warm colours at a 2px line read as one
     thing. Every street-light yellow clears 24-42 L* points from red, which is what makes it obvious.
     ⚠️ AND THE SWAP CANNOT BE THE WHOLE FIX. No yellow reaches 3:1 against a white card — yellow is
     intrinsically light (this one is 1.82:1) — so a THIN bright bar trades one confusion for the
     opposite one: too faint to tell from NO bar, which is what "current" looks like. Measured
     ΔE2000 from --line (the no-edge appearance) is 29.4, so the COLOUR is unmistakable; it is the
     AREA that had to grow. Hence the 4px bar below, and this stylesheet's own prior finding one
     screen up: "a 3px left edge alone read as almost nothing — the eye needs the block".
     Chosen by measurement over eleven candidates (L*, ΔE2000 from red, ΔE2000 from the plain border,
     WCAG contrast on both surfaces), landing in the traffic-yellow region rather than picked by eye:
       #f2b705   L* 77.7 · hue 83.5° · ΔE from --stop 45.0 · ΔL* from --stop 33.1 · ΔE from --line 29.4
       #b7791f   L* 55.9 · hue 72.7° · ΔE from --stop 26.7 · ΔL* from --stop 11.3   (what was rejected)
     🔴 NOT AN INK. It never carries text — 1.82:1 would be unreadable. --warn still owns every amber
     word and chip; this is the same hue at a different ROLE, exactly like --warn-ink above, and
     qa/paperwork-group-weight-check.php asserts the two stay in ONE hue family (within 25°) so the
     bar and the chip can never come to mean different things. */
  --warn-edge:#f2b705;
}
@media(max-width:899.98px){:root{--touch:48px}}   /* the tap-target rule */
*{box-sizing:border-box}
[hidden]{display:none !important}   /* honour the attribute even if an element sets its own display */
body{margin:0;font:var(--fs-0)/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
  color:var(--ink);background:var(--bg);-webkit-font-smoothing:antialiased}
h1{font-size:var(--fs-2);font-weight:650;margin:var(--sp-3) 0 var(--sp-1)}
h2{font-size:var(--fs-1);margin:var(--sp-3) 0 var(--sp-1)}
.muted{color:var(--muted)}
a{color:var(--accent)}
/* ── CONTENT WIDTH — capped at 1040px, centred (operator walkthrough, 2026-08-21) ──────────────
   ⚠️ THIS REVERSES the 2026-08-20 round-2 removal of the 66rem cap, deliberately and with the
   reason recorded so it is not flipped back on a third pass. That removal was correct about its
   own symptom (the old cap plus `margin:0 auto` inside the 200px+1fr shell read as an off-centre
   float with a dead left gutter) and wrong about the remedy: full bleed puts a label and the
   control it governs a foot apart on a wide monitor, which is the root cause of the pricing-form
   complaints — the radios, the field pairs, all of it. The fix is the cap AND the centring
   together, sized to the mockup: 1040px is ~66rem, so this lands where the original cap was, but
   inside the content column rather than the whole shell, which is what made it look off-centre.
   If a future walkthrough wants this reopened, the thing to change is the SHELL, not the cap. */
/* 🔴 WIDENED 1040 → 1500 (operator, 2026-09-01). At 1040 the column rendered 1040px wide on a
   1920 viewport — MEASURED, not eyeballed — leaving ~680px of window empty beside a sidebar that
   only takes 200. Cards were 1012px and card grids had room for two columns where five fit.
   🔴 NOT FULL-BLEED, deliberately: an unbounded column is what the 2026-08-20 pass removed, because
   it put a label and its own control a foot apart. 1500 is the band the operator set (1400–1600).
   🔴 THE CAP ALONE IS NOT ENOUGH — a 1500px column makes body copy unreadable (a ~170-character
   line; the typographic measure people actually read is 45–85). So the WIDTH and the MEASURE are
   two different constraints: the container gets 1500 for grids and tables, and prose is capped at
   its own readable measure just below. Change one and you must think about the other. */
main{max-width:1500px;margin:0 auto;padding:0 var(--sp-3) calc(var(--sp-5) * 2)}

/* 🔴 THE READABLE MEASURE — ONE RULE, PORTAL-WIDE. Applies to block prose only: a paragraph sitting
   directly in a card or a box. Never to grid cells, table cells or flex rows, whose width IS their
   layout. `ch` tracks the font, so this stays a measure rather than a pixel guess.
   ⚠️ If a future pass widens `main` again, this is the line that keeps the widening safe.

   🔴 WIDENED 80ch → 105ch (operator, 2026-09-02). At 80ch short card copy rendered ~611px inside a
   ~1470px card and read as broken layout rather than as a considered measure. NOT removed and NOT
   full-bleed: 1470px of prose is a ~170-character line.

   ⚠️ `ch` OVER-DELIVERS, AND THE NUMBER IN THIS RULE IS NOT THE LINE LENGTH. `ch` is the advance
   width of "0", which is wider than the average glyph, so an N-ch container renders roughly 1.15×N
   characters per line. MEASURED, not assumed: at 80ch the longest paragraph on /business rendered
   90 chars/line; at 105ch it renders ~118. Anyone tuning this must measure the LINE, not read the
   property — qa/portal-prose-measure-check.php does exactly that with Range.getClientRects(). */
main .card > p,
main .card > .hint,
main .fl-box > p,
main .fl-box > .hint{max-width:105ch}
:focus-visible{outline:2px solid var(--accent);outline-offset:1px}

/* ── CHROME: LEFT SIDEBAR (operator ruling 2026-08-13 — the admin's shape; grows
   downward forever). TEXT ONLY — no emoji (ruling 2026-08-14). Phones: the SAME
   sidebar as a hamburger drawer, pure-CSS checkbox toggle (nav never needs JS). */
.nav-toggle{display:none}
.shell{min-height:100vh}
.side{display:flex;flex-direction:column;background:var(--card);border-right:1px solid var(--line)}
.side-brand{font-weight:700;font-size:13px;letter-spacing:.02em;
  padding:var(--sp-3);border-bottom:1px solid var(--line)}
.side-nav{display:flex;flex-direction:column;padding:var(--sp-1) 0;flex:1}
.side-nav a{display:flex;align-items:center;min-height:var(--touch);
  padding:0 var(--sp-3);color:var(--muted);text-decoration:none;font-weight:500;
  border-left:2px solid transparent}
.side-nav a:hover{color:var(--ink)}
.side-nav a.on{color:var(--ink);font-weight:600;background:var(--bg);border-left-color:var(--accent)}
.side-foot{padding:var(--sp-2) var(--sp-3);border-top:1px solid var(--line);
  display:flex;align-items:center;justify-content:space-between;gap:var(--sp-2);font-size:12px}
.side-foot .muted{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.bar{display:flex;gap:var(--sp-3);align-items:center;
  padding:var(--sp-1) var(--sp-3);background:var(--card);border-bottom:1px solid var(--line)}
.bar .spacer{margin-left:auto}
.hamb{display:inline-flex;align-items:center;justify-content:center;min-width:var(--touch);
  min-height:var(--touch);font-size:18px;cursor:pointer;border-radius:var(--r)}
@media(min-width:900px){
  .shell{display:grid;grid-template-columns:200px minmax(0,1fr)}
  .side{position:sticky;top:0;height:100vh}
  .phone-bar{display:none}
  .nav-scrim{display:none !important}
}
@media(max-width:899.98px){
  .phone-bar{position:sticky;top:0;z-index:5}
  .side{position:fixed;top:0;bottom:0;left:0;width:min(72vw,280px);z-index:11;
    transform:translateX(-105%);transition:transform .15s ease;
    box-shadow:4px 0 20px rgba(26,29,33,.14)}
  .nav-toggle:checked ~ .shell .side{transform:none}
  .nav-scrim{display:none;position:fixed;inset:0;background:rgba(26,29,33,.3);z-index:10}
  .nav-toggle:checked ~ .shell .nav-scrim{display:block}
}
@media(prefers-reduced-motion:reduce){.side{transition:none}}

/* ── buttons: quiet by default; semantic colors mean commit/destructive only ── */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-1);
  min-height:var(--touch);padding:0 var(--sp-2);border:1px solid var(--line);border-radius:var(--r);
  background:var(--card);color:var(--ink);font:inherit;font-size:13px;font-weight:600;
  cursor:pointer;text-decoration:none}
.btn:hover{border-color:var(--muted)}
.btn-go{background:var(--go);border-color:var(--go);color:#fff}
/* 🔴 THE CONTROL VOCABULARY (operator rulings 2026-08-20/21) — four kinds, four looks, NO new ones:
     solid green  (.btn-go)     — THE one primary save per page
     solid blue   (.btn-up)     — an UPLOAD (a different kind of action)
     outlined blue (.btn-assist) — an ASSIST: fills, copies, or reveals; never saves or uploads
                                   ("anything clickable that fills, copies, or reveals should
                                   LOOK clickable" — plain-text assists don't get clicked)
     quiet        (.btn-quiet)  — secondary navigation / stay-here
   Never two same-green siblings; never a new treatment invented for one control. */
.btn-up{background:#2b6cb0;border-color:#2b6cb0;color:#fff}
.btn-assist{background:transparent;border:1px solid #2b6cb0;color:#2b6cb0}
.btn-assist:hover{background:#eaf2fb;border-color:#2b6cb0}
.btn-stop{background:var(--stop);border-color:var(--stop);color:#fff}
.btn-quiet{border-color:transparent;background:transparent;color:var(--muted)}
.btn-wide{display:flex;width:100%}
.btn-sticky{position:fixed;left:var(--sp-3);right:var(--sp-3);bottom:calc(var(--sp-3) + env(safe-area-inset-bottom));
  margin:0;z-index:6}

/* ── cards: hairline, small radius, compact ─────────────────────────────────── */
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--r);
  padding:var(--sp-2) var(--sp-3);margin:var(--sp-2) 0}
.card h2{font-size:13px;font-weight:650;margin:0 0 2px}
.card .hint{margin:0 0 var(--sp-1)}
.hint{color:var(--muted);font-size:12px}
.row{display:flex;align-items:center;gap:var(--sp-2);min-height:var(--touch)}
.row .grow{flex:1;min-width:0}
/* FIELD GRIDS STACK ON A PHONE (2026-08-19, operator: "city/state/zip stack on top of each other …
   any multi-column row must stack cleanly on a phone, not overlap"). Structural, not per-view: only
   a .row that actually holds a text-like input/select collapses to one column — nav (‹ ›), the
   "Repeats weekly" checkbox, and list lines (<strong class=grow>) also use .row and are SPARED.
   :has() is baseline since 2023 (safe on any truck owner's phone). */
@media(max-width:560px){
  .row:has(> label > input:not([type=checkbox]):not([type=radio]):not([type=button])),
  .row:has(> label > select),
  .row:has(> input:not([type=checkbox]):not([type=radio]):not([type=button]):not([type=submit])),
  .row:has(> select){flex-direction:column;align-items:stretch;min-height:0}
  .row:has(> label > input:not([type=checkbox]):not([type=radio]):not([type=button])) > *,
  .row:has(> label > select) > *,
  .row:has(> input:not([type=checkbox]):not([type=radio]):not([type=button]):not([type=submit])) > *,
  .row:has(> select) > *{width:100%;flex:none}
}
.rowcard{border-left:3px solid var(--line)}
.dot{display:inline-block;width:.55em;height:.55em;border-radius:50%;flex:none}
/* 🔴 BRAND CARD GRID (2026-09-01, rebuilt from the loose stacks it replaced).
   auto-fit + a 270px minimum: the column count FOLLOWS the width and is never declared — 5 across at
   the 1500px cap, fewer as it narrows, 1 on a phone.
   ⚠️ THE MINIMUM IS 270, NOT THE ~190 FIRST SKETCHED. Both were specified — "minmax(~190px)" and
   "5 across at 1500px" — and they are not compatible: at 190 the 1444px inner width fits SEVEN
   columns (measured), which also leaves each card too narrow for a logo, a name, a description and
   a full-width button. 270 is the value that delivers the stated OUTCOME (1444/270 = 5.3 → 5, and
   six would need ≤240). The outcome was taken as the intent; the number followed from it. 🔴 The fleet is heading for ~62 concepts, so
   nothing here may assume ten: no nth-child rules, no fixed track list, no per-brand CSS.
   ⚠️ auto-FIT here (not auto-fill, which the earlier stack-grid used): these cards should share the
   row evenly rather than leave phantom empty tracks, and a card is never alone on this screen —
   the brand catalogue always has several. */
.bgrid{display:grid;gap:var(--sp-2);grid-template-columns:repeat(auto-fit,minmax(270px,1fr));align-items:stretch}
/* 🔴 THE VENDOR GRID FILLS, IT DOES NOT FIT. `auto-fit` collapses empty tracks, so a vendor with ONE
   brand got a single card stretched to the full 1442px row — a banner beside ten 280px cards, which
   is the opposite of the "same shape" the ruling asks for. `auto-fill` keeps the empty tracks, so
   the first card is the same width as one of ours whether the vendor owns one brand or six.
   (Measured: 1442px -> 280px, identical to an FTA card.) */
#mine .bgrid{grid-template-columns:repeat(auto-fill,minmax(270px,1fr))}

/* 🔴 TWO DIFFERENT ALIGNMENTS, TWO DIFFERENT PROPERTIES — corrected 2026-09-01 by a plant that
   found no teeth, which is the only reason the mistake was caught:
     · EQUAL CARD HEIGHT across a row is `align-items:stretch` on the GRID (above). Grid items
       stretch to the tallest in their row on their own.
     · ALIGNED BUTTONS across a row is `.bcard__desc{flex:1}` below. The description absorbs the
       slack inside an already-equal-height card, so every action sits on the same line.
   The first draft of this comment credited the flexing description with the card height. It does
   not do that: removing flex:1 leaves the heights equal and only ragged the buttons. Both
   properties are load-bearing, for different claims — qa/brand-cards-check.php asserts them
   separately (B3 height, B3b buttons) with a plant each. */
.bcard{display:flex;flex-direction:column;gap:var(--sp-1);
  border:1px solid var(--line);border-radius:12px;background:var(--card);padding:var(--sp-3);min-width:0}
.bcard--active{border-color:var(--go)}
.bcard__top{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--sp-1)}
/* Fixed square tile — a wide logo and a narrow one occupy the SAME footprint, which is what keeps
   the names and buttons on a row aligned. contain, so neither is cropped. */
.bcard__tile{width:40px;height:40px;flex:0 0 40px;border-radius:8px;background:var(--off);
  display:flex;align-items:center;justify-content:center;overflow:hidden}
.bcard__tile img{max-width:32px;max-height:32px;object-fit:contain}
.bcard__name{font-size:var(--fs-0);font-weight:650;margin:0;line-height:1.3}
/* flex:1 IS the equal-height mechanism — see the .bcard note. Keep it. */
.bcard__desc{flex:1;margin:0;color:var(--muted);font-size:12px;line-height:1.4;min-height:2.8em}
/* 🔴 THE DESCRIPTION RESERVES TWO LINES, and that is what makes the two grids agree. `align-items:
   stretch` equalises cards WITHIN a grid; it cannot reach across two. Our brands' blurbs wrap to two
   lines and a vendor's one-line status did not, so vendor cards measured 17px shorter — exactly one
   line — and their button sat 16px higher. A floor on the description equalises them structurally
   rather than pinning a card height that would go stale.
   ⚠️ If a blurb ever wraps to THREE lines the two grids diverge again and V6 goes red. That is the
   leg working: it means the shapes really have parted, not that the tolerance is wrong. */
.bcard__note{margin:0;font-size:11px;line-height:1.35;color:var(--warn)}
.bcard__note--quiet{color:var(--muted)}
.bcard__act{margin-top:var(--sp-1)}
/* 2026-09-08: a badge WRAPS inside its card at every width (it ran past the edge with the "N to price" text) — never nowrap */
.bbadge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:650;white-space:normal;max-width:100%;min-width:0;overflow-wrap:anywhere;line-height:1.3}
.bcard__top .bbadge{flex:1 1 auto;text-align:right}
/* 🔴 STILL A BADGE, NOT A BUTTON — a pill with no border, no hover, no cursor change. It is a
   STATE. And amber, not red: "Needs pricing" means incomplete, never broken. */
.bbadge--go{background:var(--go-bg-strong);color:var(--go-ink)}
/* 🔴 RED, and STILL A BADGE — a pill with no border, no hover, no cursor change. A state, not an
   action. (.bbadge--warn keeps its name: the class is the STATE it marks, not the colour it uses.) */
.bbadge--warn{background:var(--stop-bg-strong);color:var(--stop-ink)}

.cardgrid{display:grid;gap:var(--sp-2);grid-template-columns:1fr}
@media(min-width:700px){.cardgrid{grid-template-columns:1fr 1fr;align-items:start}
  .cardgrid .card{margin:0}}
.truckline{display:flex;align-items:baseline;gap:var(--sp-1);color:var(--muted);
  min-height:auto;flex-wrap:wrap;font-size:13px;margin:2px 0}
.actline{margin:3px 0;font-size:13px;color:var(--muted)}   /* attention lines: inline flow, marker never orphans */
.side-foot .btn{white-space:nowrap}
.stat{font-size:var(--fs-2);font-weight:650;color:var(--ink);font-variant-numeric:tabular-nums}
.dims{display:flex;align-items:center;gap:var(--sp-1)}
.dims input{width:5.5em;text-align:center}

/* ── verdict chips: small, quiet, semantic ──────────────────────────────────── */
.chip{display:inline-block;border-radius:3px;padding:1px 7px;
  font-size:11px;font-weight:600;background:var(--off);color:var(--muted)}
.chip-open{background:var(--go-bg);color:var(--go)}
.chip-booked{background:var(--stop-bg);color:var(--stop)}
.chip-mine{background:var(--warn-bg);color:var(--warn)}
.chip-off{background:var(--off);color:var(--muted)}
.chip-past{background:transparent;color:var(--muted);border:1px solid var(--line)}

/* ── card-state banners (operator rules 2026-08-20/21) ──────────────────────────
   THREE states, three visual treatments, all PROMINENT and FIRST on the card —
   a vendor scans the page and knows instantly which items are done, which are
   waiting on us, and which need them. Full words, never the initials "FTA":
     .waitline (amber) — waiting on Food Truck Avenue
     .okline   (green) — approved, nothing more needed
     .fixline  (red)   — needs the vendor's action
     .dueline  (amber) — renewal due soon (the admin's own 60-day expiring window +
                         colour, mirrored vendor-side — operator 2026-08-21); renders
                         BENEATH the state banner: approved-and-expiring keeps the
                         green truth and adds the amber warning, never replaces it */
.waitline,.okline,.fixline,.dueline{display:block;margin:0 0 var(--sp-2);padding:10px 14px;
  border-radius:10px;color:var(--ink);font-weight:650;line-height:1.35}
/* 🔴 WAITING ON US IS A STATUS, NOT A WARNING (operator ruling 2026-09-08). .waitline was amber —
   the same amber as .dueline, which IS a prompt to act. On the Paperwork step the two stacked:
   "waiting on Food Truck Avenue to review" (nothing to do) directly above "renewal due soon"
   (something to do), in identical colour. Two states that ask opposite things of a vendor cannot
   share a colour, or the colour stops meaning anything and both get ignored.
   Neutral: the card's own ground and a plain rule, so it reads as information. Amber is now
   reserved for .dueline and the badges that genuinely want a response.
   ⚠️ GLOBAL, deliberately — .waitline carries every waiting-on-us banner in the portal (W-9,
   liability, photos, brand photos, paperwork). Leaving the other seven amber would contradict the
   ruling's own reasoning the moment a vendor saw one beside a real prompt. */
.waitline{background:var(--bg);border:1px solid var(--line);font-weight:600}
.okline{background:var(--go-bg);border:1px solid var(--go)}
.fixline{background:var(--stop-bg);border:1px solid var(--stop)}
.dueline{background:var(--warn-bg);border:1px solid var(--warn)}
.waitline .sub,.okline .sub,.fixline .sub,.dueline .sub{display:block;font-weight:400;color:var(--muted);font-size:.88em;margin-top:2px}

/* ── "Getting set up" onboarding strip (approved 2026-08-21) — visible everywhere while
   incomplete, gone when done. The CURRENT step is the one live link. ─────────────── */
.onb-strip{display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px;margin:0 0 var(--sp-3);
  padding:8px 14px;border:1px solid var(--line);border-radius:10px;background:var(--panel,#fafafa);
  font-size:.86em}
.onb-strip__t{font-weight:700;color:var(--muted);text-transform:uppercase;font-size:.78em;letter-spacing:.04em}
.onb-step{color:var(--muted);white-space:nowrap}
.onb-step.is-done{color:var(--go)}
.onb-step.is-unk{color:var(--warn)}
a.onb-step.is-cur{color:var(--accent,#1a6);font-weight:700;text-decoration:underline}
/* Completed steps became links 2026-09-01 (breadcrumbs clickable for done + current only). They
   carry an underline so the affordance is VISIBLE — a green tick that happens to be clickable and
   looks identical to one that is not teaches the vendor nothing. Weight stays normal so `→ current`
   remains the loudest thing in the strip; the ○ not-yet-reached steps stay plain <span>s and get no
   underline because there is no link to give them. */
a.onb-step.is-done{text-decoration:underline}

/* ── INLINE BRAND PRICING PANEL (operator ruling 2026-09-02) ─────────────────────────────────────
   🔴 THE HOST SITS AFTER .bgrid AND IS NOT A GRID ITEM. That is the whole mechanism: the panel is
   appended here, outside the grid's box, so no card's geometry changes when it opens. Putting the
   panel inside .bgrid would make it a grid item and reflow every card — the exact defect the ruling
   names ("cards do not collapse, reflow, or reorder when a panel opens").
   No transition on height and no scroll behaviour is declared: an animation that moves the page is
   how the vendor loses their place, and holding scroll position is part of the ruling. */

/* 🔴 THE DIVIDER BETWEEN THE TWO WAYS TO SELL (operator, 2026-09-02) — a rule with generous space,
   never the word "or": a vendor can build their own brand AND run ours, so "or" would state a
   choice that does not exist. The space either side is doing as much work as the line. */
.brand-split{border:0;border-top:1px solid var(--line);margin:var(--sp-5) 0}
/* the one-line pricing summary near the intro; the full copy sits below BOTH sections */
.pricing-line{margin-top:var(--sp-2)}
.brand-panel-host{margin-top:var(--sp-4)}
.brand-panel-host:empty{display:none}
/* 🔴 WHERE A STEP CHANGE LANDS (operator standing rule 2026-09-16: "pin the transition to the top of the section").
   Both wizards scroll their panel into view with block:'start' on a step change; this is the only place that says how much
   air to leave and what has to stay clear. --touch is the phone's sticky .phone-bar (48px under 900px, 32px above it), so
   the arrival is never tucked under it. 🔴 THE NUMBER LIVES HERE, NEVER IN A MODULE: truck-panel.js and
   brand-wizard.js both call scrollIntoView and nothing else, so a layout change is one edit, not three. It is sliced into
   the admin's own stylesheet from this rule (.pt-panel is in FTA_BW_CSS_CLASSES), which is what keeps the two surfaces
   landing in the same place. */
.pt-panel{scroll-margin-top:calc(var(--touch, 48px) + var(--sp-3, 14px))}
/* 🔴 THE OPEN PANEL IS ITS OWN BLOCK, NOT MORE PAGE (operator ruling 2026-09-02).
   Bare `.pt-panel` is styled for the TRUCK page — a dashed top rule on a near-white ground, which
   is right inside an accordion and wrong here: on /brands it read as another slab of page content
   rather than as the thing that just opened. The dimmed cards receded and nothing came forward.
   Everything below is scoped to .brand-panel-host so the truck page is untouched, by ruling.
   🔴 BLUE IS var(--accent) — THE PORTAL'S EXISTING BLUE, not a new one. It is already the link and
   focus colour, so it reads as informational; green (--go) is the action colour on every button and
   would compete, red (--stop) reads as error. A SECOND blue would be worse than reusing this one. */
.brand-panel-host .pt-panel{
	display:block;
	/* 🔴 A TINTED SURFACE, NOT WHITE. White-on-off-white measured 255 vs 246 — technically distinct
	   and visually nothing, because every CARD on the page is white too, so the panel read as one
	   more of them. The tint is a wash of the SAME accent, so surface and border are one idea.
	   ⚠️ ITS LUMINANCE IS CLOSE TO WHITE ON PURPOSE (the page stays light) — which is exactly why
	   qa/brand-panel-check.php D3 compares COLOUR, not mean grey: a grey-only test would pass a tint
	   nobody can see. */
	background:var(--accent-bg);
	/* 🔴 ALL FOUR SIDES, ~2px. A 3px left edge alone read as almost nothing — the eye needs the block
	   to be enclosed before it reads as a block. */
	border:2px solid var(--accent);
	border-radius:var(--r);
	padding:var(--sp-4);
	box-shadow:0 2px 8px rgba(26,29,33,.10);
}
/* the package table and menu keep a white ground so their rows stay legible against the tint */
.brand-panel-host .pkgs,.brand-panel-host .pfields{background:var(--card)}
/* room to breathe inside — the menu grid and the package table were sitting on the edges */
.brand-panel-host .pkg-menu{margin-bottom:var(--sp-3);padding-bottom:var(--sp-3)}
.brand-panel-host .pkgs,.brand-panel-host .pfields{margin-top:var(--sp-3)}
.brand-panel-host form>.hint:first-child{margin-top:0}
/* 🔴 THE OPEN CARD IS BLUE, THE SAME BLUE AS ITS PANEL (operator ruling 2026-09-02). It was green,
   which put a green card above a blue panel and read as two unrelated things — the card and the
   panel it opened have to read as ONE unit. `border-color` moves too, so a card that is BOTH priced
   (.bcard--active, green) and open does not show a green ring around a blue-anchored panel.
   ⚠️ THE BADGES DO NOT CHANGE: "Active brand" stays green and "Needs pricing" stays amber. Those
   carry STATE, which is a different job from showing which card is open. */
/* 🔴 WEIGHT WITHOUT LAYOUT. The card's border stays 1px — widening it to 2px would change the box
   and REFLOW the grid, which the ruling forbids outright. An OUTSET ring paints 2px of accent around
   the card and occupies no space, so the card reads at the same weight as the panel border while
   every rect stays byte-identical (asserted by G4 and by D-r1). */
.bcard.is-open{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent)}

/* 🔴 "Change pricing" IS AN ACTION AND MUST LOOK LIKE ONE (operator, 2026-09-02). It rendered as
   .btn-quiet — bare text beside solid-green Join brand / Set pricing — so scanning the grid, every
   joined card looked like it had no action at all.
   OUTLINED, not filled: it stays secondary to the green primary actions, and it is the SAME
   var(--accent) as the panel it opens, so the button, the card ring and the panel are one idea.
   ⚠️ .btn-assist already exists as an outlined blue, but on a hardcoded #2b6cb0 — a THIRD blue. Not
   reused, and not changed here: it belongs to another surface and is out of scope. */
.btn-accent{background:transparent;border-color:var(--accent);color:var(--accent);font-weight:650}
.btn-accent:hover{background:var(--accent-bg);border-color:var(--accent)}
/* OUTLINED STOP (operator ruling 2026-09-06): "Remove from this truck" wore .btn-quiet — transparent
   border on transparent ground in --muted ink — which read as caption text, not a control. It is a real
   action and must look like one, but NOT solid --stop: removing a menu from a truck is reversible
   (flip-never-delete), and the solid fill is reserved for the destructive weight. Same construction as
   .btn-accent above, on the stop hue — outline, hue-matched ink, tinted hover. */
.btn-stop-outline{background:transparent;border-color:var(--stop);color:var(--stop);font-weight:650}
.btn-stop-outline:hover{background:var(--stop-bg);border-color:var(--stop)}

/* The opt-out. Secondary weight, well below the pricing button — this is not an action we push. */
.bcard__leave{margin:var(--sp-2) 0 0;text-align:center;font-size:11.5px;line-height:1.3}
/* 🔴 RED TEXT, COLOUR ONLY (operator, 2026-09-02). Same size, same underline, same position — this
   is a destructive action we do not want encouraged, and button weight would make it the most
   prominent thing on a joined card, competing with Change pricing. Red text signals destructive
   without pulling the eye.
   ⚠️ var(--stop) IS THE EXISTING DANGER TOKEN — the same one this link already used on :hover, and
   the one .bcard__confirm borders with. No new hex: a fourth ad-hoc colour on this page is exactly
   how the third blue (.btn-assist's hardcoded #2b6cb0) happened. */
.bcard__leave a{color:var(--stop);text-decoration:underline}
.bcard__leave a:hover{color:var(--stop);text-decoration-thickness:2px}
/* the confirm block replaces the action while it is open, so the card cannot be left by one click */
.bcard__confirm{margin-top:var(--sp-2);padding:var(--sp-2);border:1px solid var(--stop);
	border-radius:var(--r);background:var(--stop-bg)}
.bcard__confirm p{margin:0 0 var(--sp-2);font-size:11.5px;line-height:1.35;color:var(--ink)}
.bcard__confirm .row{display:flex;gap:var(--sp-1)}
.bcard__confirm .row form,.bcard__confirm .row a{flex:1}
.bcard__confirm .btn{width:100%}
/* THE VENDOR LOGO CARD (2026-09-18, operator: "make it look more like a card … I can't tell if or when my file is taken"). ONE shared
   partial on both dashboards (tools/lib/brand-wizard/views/_vendor_logo_card.php): a preview slot BESIDE the uploader, the uploader
   itself the SAME markup as the photo uploader (photo-form.js reads dz-count / dz-previews / photo-status / uploader__progress — the card
   used to carry the paperwork uploader's element names, so a picked file showed nothing at all). Existing tokens only. */
.vlogo{padding:var(--sp-3);border-radius:10px;box-shadow:0 1px 3px rgba(20,30,40,.08)}
.vlogo__body{display:flex;gap:var(--sp-3);align-items:flex-start;flex-wrap:wrap;margin-top:var(--sp-2)}
.vlogo__preview{flex:0 0 220px;min-height:132px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--sp-1);
	border:1px solid var(--line);border-radius:var(--r);background:#fff;padding:var(--sp-2);text-align:center}
.vlogo__preview img{max-width:196px;max-height:100px;display:block}
.vlogo__preview .vlogo__cap{font-size:11.5px;color:var(--muted);margin:0}
.vlogo__form{flex:1 1 320px;min-width:0}
.vlogo__form .dropzone{padding:var(--sp-2) var(--sp-3)}


/* ── DIM THE NON-ACTIVE CARDS WHILE A PANEL IS OPEN (operator ruling 2026-09-02) ──────────────────
   The open card is the anchor for the panel below it, so everything else recedes.
   🔴 OPACITY ONLY. No `pointer-events`, no `filter`, no `display` — a dimmed card MUST stay
   clickable (opening another brand still works and still closes the first), and pointer-events:none
   would break that silently while looking identical in a screenshot.
   🔴 0.55 IS A FLOOR, NOT A TASTE. Below ~0.5 the amber "Needs pricing" badge stops being legible at
   a glance, which is the thing the ruling explicitly protects. qa/brand-panel-check.php D3 asserts
   the band, so a later "just a bit more" cannot quietly cross it.
   The transition lives on .bcard itself, not on the dimmed state, so it applies in BOTH directions —
   on a state-only transition the restore would snap. */
.bcard{transition:opacity .14s ease}
.bgrid--dimmed .bcard{opacity:.55}
.bgrid--dimmed .bcard.is-open{opacity:1}
@media (prefers-reduced-motion: reduce){ .bcard{transition:none} }

[data-panel-ok]{color:var(--go)}
[data-panel-err]{color:var(--stop)}

a.onb-step.is-done:hover,a.onb-step.is-cur:hover{text-decoration-thickness:2px}
.onb-forward{margin:0 0 var(--sp-2)}

/* ── brand ROWS + panel extras (operator-approved 2026-08-21: the LIST kept — "tiles look
   better and scan worse" — upgraded with information; admin DENSITY matched with portal-own
   rules, never the admin stylesheet). No amber on this list on purpose: nothing here ever
   waits on us — the marketplace ruling made visible. ─────────────────────────────── */
.pt-brow{display:grid;grid-template-columns:40px minmax(0,1fr) max-content;align-items:center;
  gap:12px;padding:8px 0;border-bottom:1px solid var(--line)}
.pt-brow form{justify-self:end}
.pt-brow form .btn{min-width:6.5em;justify-content:center}
.pt-brow:last-child{border-bottom:0}
.pt-bdisc{width:40px;height:40px;border-radius:10px;flex:0 0 40px;object-fit:cover;
  display:inline-flex;align-items:center;justify-content:center;font-weight:700;font-size:.95em}
.pt-menu{margin-top:var(--sp-2)}
.pt-menu>summary{cursor:pointer;color:var(--muted);font-weight:600}

/* the recoverable feed link (Sync) — monospace, masked until Reveal */
.linkbox{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;
  padding:6px 8px;border:1px solid var(--line);border-radius:var(--r);
  background:var(--bg);color:var(--ink);min-width:0;overflow:hidden;text-overflow:ellipsis}
/* locked address components — filled by a pick or the explicit manual escape */
.geo-locked{background:var(--bg);color:var(--muted);cursor:not-allowed}

/* ── inputs ─────────────────────────────────────────────────────────────────── */
input,select{font:inherit;font-size:13px;min-height:var(--touch);padding:0 var(--sp-2);
  border:1px solid var(--line);border-radius:var(--r);background:var(--card);
  color:var(--ink);width:100%}
label{display:block;margin:var(--sp-1) 0;font-weight:600;font-size:12px}
label small{display:block;font-weight:400;color:var(--muted)}

/* ── the 3-step sheet (schedule) ────────────────────────────────────────────── */
.sheet{position:fixed;inset:auto 0 0 0;z-index:10;background:var(--card);
  border-radius:6px 6px 0 0;box-shadow:0 -6px 24px rgba(26,29,33,.16);
  padding:var(--sp-3) var(--sp-3) calc(var(--sp-3) + env(safe-area-inset-bottom));
  max-height:88vh;overflow:auto}
.sheet[hidden]{display:none}
.scrim{position:fixed;inset:0;background:rgba(26,29,33,.3);z-index:9}
.scrim[hidden]{display:none}
.dots{display:flex;gap:5px;justify-content:center;margin-bottom:var(--sp-2)}
.dots i{width:7px;height:7px;border-radius:50%;background:var(--line)}
.dots i.on{background:var(--accent)}
.choice{display:flex;flex-direction:column;gap:var(--sp-2)}
.choice .btn{min-height:52px;justify-content:flex-start;text-align:left}
.choice .btn em{font-style:normal;font-weight:400;color:var(--muted);display:block;font-size:.9em}
.choice .btn.on{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}
.pills{display:flex;gap:var(--sp-1);flex-wrap:wrap}
.pills .btn{min-width:var(--touch);padding:0 var(--sp-2)}
.pills .btn.on{background:var(--accent);border-color:var(--accent);color:#fff}
.suggest{position:relative}
.suggest .out{margin-top:var(--sp-1)}
.suggest .out .btn{width:100%;justify-content:flex-start;font-weight:400;margin:3px 0}

/* ── calendar: week strip + agenda + month grid ─────────────────────────────── */
.strip{display:grid;grid-template-columns:repeat(7,1fr);gap:4px;margin:var(--sp-2) 0}
.strip a{display:flex;flex-direction:column;align-items:center;gap:1px;min-height:var(--touch);
  padding:4px 0;border-radius:var(--r);text-decoration:none;color:inherit;font-size:12px}
.strip a.today{box-shadow:inset 0 0 0 1px var(--accent)}
.strip a.sel{background:var(--card);border:1px solid var(--line)}
.strip .d{font-size:.78em;color:var(--muted)}
.strip .dots-row{display:flex;gap:2px;min-height:7px}
.mgrid{display:none}
@media(min-width:700px){
  .mgrid{display:grid;grid-template-columns:repeat(7,1fr);gap:4px}
  .mgrid .cell{background:var(--card);border:1px solid var(--line);border-radius:var(--r);
    min-height:74px;padding:4px 5px;font-size:.8em;overflow:hidden}
  .agenda-only{display:none}
}
.empty{padding:var(--sp-4) var(--sp-3);text-align:center}
.empty p{color:var(--muted)}

/* ── money ledger (2026-08-17) — the portal's first table; business-tool treatment:
     hairlines, right-aligned money, and the WRAPPER scrolls on phones, never the page. */
.tablewrap{overflow-x:auto;padding:var(--sp-1) 0}
table.ledger{width:100%;border-collapse:collapse;font-size:var(--fs-0);min-width:640px}
table.ledger th,table.ledger td{padding:5px var(--sp-2);text-align:left;vertical-align:top;
  border-bottom:1px solid var(--line);white-space:nowrap}
/* Column 3 was Truck (a text column) until 2026-09-14; it is now Vendor payable, which is
   numeric and must not be given a text column's 11em min-width or wrapping. Only the Job
   column (2) is prose now. */
table.ledger td:nth-child(2){white-space:normal;min-width:11em}
table.ledger th{font-weight:600;color:var(--muted);font-size:12px;
  text-transform:uppercase;letter-spacing:.03em}
/* per-column sort arrows: the arrows ARE the controls (labels are plain text) */
table.ledger th .sortarrows{display:inline-flex;flex-direction:column;vertical-align:middle;
  margin-left:4px;line-height:.85}
table.ledger th .sortarrows a{color:#c3c7cc;text-decoration:none;font-size:8px;
  padding:0 3px;text-transform:none}
table.ledger th .sortarrows a:hover{color:var(--accent)}
table.ledger th .sortarrows a.on{color:var(--accent)}
table.ledger .num{text-align:right;font-variant-numeric:tabular-nums}
table.ledger tr.voided td{color:var(--muted);text-decoration:line-through}
table.ledger tr.voided td .muted{text-decoration:none}
table.ledger tr.totals td{font-weight:650;border-top:2px solid var(--ink);border-bottom:0}
/* the ONE document-history list (shared renderer with the admin, 2026-08-14) */
.doc-hist{margin:4px 0 8px;font-size:12px}
.doc-hist summary{cursor:pointer;color:var(--muted);font-weight:600}
.doc-hist ul{margin:4px 0 0;padding-left:1.1em}
.doc-hist li{margin:2px 0}
.doc-hist-meta{color:var(--muted)}

/* geo-suggest (2026-08-14) — THE address autocomplete's dropdown (vendored geo-suggest.js;
   inline suggests deleted, one-component ruling). Portal tokens: surface = --card, error =
   --stop (this sheet has no --panel/--err — map, don't invent). The three states (hits /
   note / err) must stay visually distinct — the white-on-white round's ruling, both apps. */
.geo-suggest-out{position:relative;z-index:20}
.geo-suggest-out:empty{display:none}
.geo-suggest-hit{display:block;width:100%;text-align:left;padding:.45em .6em;border:1px solid var(--line);
	border-top:none;background:var(--card);color:var(--ink);cursor:pointer;font:inherit;font-weight:400}
.geo-suggest-hit:first-child{border-top:1px solid var(--line)}
.geo-suggest-hit:hover{background:var(--bg)}
.geo-suggest-note{display:block;padding:.45em .6em;border:1px solid var(--line);
	background:var(--card);color:var(--muted);font-style:italic}
.geo-suggest-err{display:block;padding:.45em .6em;border:1px solid var(--stop);
	background:var(--card);color:var(--stop)}
/* "Did you mean…?" — step 2 (2026-09-12), the portal twin of the admin's block. A proposed
   correction is its own state: not a result, not an error. Accepting it runs the same applyHit()
   a suggestion click runs, so it leaves the same pick evidence the server checks on save. */
.geo-suggest-dym{display:block;border:1px solid var(--accent);border-radius:6px;overflow:hidden}
.geo-suggest-dym-q{padding:.45em .6em;background:var(--bg);color:var(--ink);font-weight:600}
.geo-suggest-dym .geo-suggest-hit{border-left:none;border-right:none}
.geo-suggest-dym-no{display:block;width:100%;text-align:left;padding:.4em .6em;border:none;
	border-top:1px solid var(--line);background:var(--card);color:var(--muted);cursor:pointer;font:inherit;font-size:.92em}
.geo-suggest-dym-no:hover{background:var(--bg);color:var(--ink)}

/* ══ Pricing card (gate 6, 2026-08-18) — the completeness banner, both-states commission box,
   and worked example. Text-only surfaces: every string is server-built (Pricing::previewPayload). */
.price-banner{border-radius:var(--r);padding:var(--sp-2) var(--sp-3);margin:var(--sp-2) 0;font-weight:600}
.price-banner--gap{background:#fdf3d6;border:1px solid #e0a800}
.price-banner--ok{background:var(--go-bg);border:1px solid var(--go)}
.price-banner--err{background:var(--stop-bg);border:1px solid var(--stop)}
.price-live{border:1px solid var(--go);border-radius:var(--r);padding:var(--sp-2) var(--sp-3);
	margin:var(--sp-2) 0 var(--sp-1);background:var(--go-bg)}
.price-live__basis{font-size:var(--fs-0)}
.price-live__line{margin:2px 0;opacity:.55}
.price-live__line.is-active{opacity:1}
.price-live__tag{display:inline-block;min-width:7em;text-align:center;font-size:.72em;
	text-transform:uppercase;letter-spacing:.04em;border:1px solid var(--go);border-radius:4px;
	padding:.05em .4em;margin-right:.5em}
.price-live__line.is-active .price-live__tag{background:var(--go);color:#fff}
.price-example__body{border-left:3px solid var(--go);padding:2px var(--sp-3);margin-top:var(--sp-1)}
/* the brand panel's worked example + the sized-brand explanation (2026-09-03) — same 720px column as
   the package table above them, so the example reads as part of the table, not a footer */
.pkg-example{max-width:720px;margin-top:var(--sp-2)}
.pkg-example__h{text-transform:uppercase;letter-spacing:.05em;font-size:.75em}
.pkg-example__body div+div{margin-top:2px}
.pkg-sized{max-width:720px;margin:var(--sp-1) 0 var(--sp-2);font-size:13px;line-height:1.45}
.price-form .row{gap:var(--sp-3)}

/* ══ Get-listed checklist (2026-08-19) — the path a new vendor follows, derived from the
   readiness registry. NEXT prominent, the rest below; FTA-owned gaps read as waiting, not failure. */
.getlisted{border:2px solid var(--go);background:var(--go-bg)}
.getlisted h2{font-size:15px}
.gl-next{display:flex;align-items:center;flex-wrap:wrap;gap:var(--sp-2);margin:var(--sp-2) 0;
	font-size:var(--fs-1);padding:var(--sp-2) var(--sp-3);background:#fff;border-radius:var(--r);border:1px solid var(--go)}
.gl-next .btn{margin-left:auto}
.gl-mark{color:var(--go);font-weight:700;font-size:1.2em}
.gl-done{color:var(--go);font-weight:600;margin:var(--sp-2) 0}
.gl-wait{color:var(--muted);margin:var(--sp-2) 0}
.gl-truck{margin:var(--sp-2) 0;padding-top:var(--sp-1)}
.gl-truck h3{font-size:13px;font-weight:650;margin:var(--sp-2) 0 2px}
.gl-list{list-style:none;padding:0;margin:var(--sp-1) 0}
.gl-list li{padding:3px 0;line-height:1.5}
.gl-list li a{margin-left:.4em}
.gl-list li.gl-check{color:var(--muted)}
.gl-list li.gl-dim{color:var(--muted);font-style:italic}
.gl-arm-note{margin-top:var(--sp-3);border-top:1px solid var(--line);padding-top:var(--sp-2)}

/* ══ Document card + drop-zone pattern (2026-08-19) — the liability/paperwork upload, refreshed
   within the portal's own density. Theme tokens only; mobile-first (a truck owner on a phone). */
.doc-card__head{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap}
.doc-card__head h2{margin:0;flex:1;min-width:0}
.doc-card__state{margin:var(--sp-1) 0 var(--sp-2)}
.dropzone{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;
	gap:var(--sp-1);text-align:center;padding:var(--sp-4) var(--sp-3);margin:var(--sp-2) 0;
	border:2px dashed var(--line);border-radius:var(--r);background:var(--bg);color:var(--muted);
	cursor:pointer;transition:border-color .12s,background .12s}
.dropzone:hover,.dropzone.is-over{border-color:var(--go);background:var(--go-bg);color:var(--ink)}
.dropzone.has-file{border-style:solid;border-color:var(--go);background:var(--go-bg)}
.dropzone input[type=file]{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer}
.dropzone__icon{font-size:1.4em;color:var(--go)}
.dropzone__prompt u{color:var(--go);text-underline-offset:2px}
.dz-picked{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap;justify-content:center}
.dz-picked:empty{display:none}
.dz-thumb{width:44px;height:44px;object-fit:cover;border-radius:6px;border:1px solid var(--line)}
.dz-meta{font-weight:700;color:var(--go);word-break:break-all}   /* ✓ Attached: <file> — green, unmissable */
.dz-swap{flex-basis:100%;text-align:center;color:var(--muted);font-size:11px;font-weight:400}
/* Once a file is picked, the "drag here" prompt is REPLACED by the ✓ Attached state — so the card
   visibly changes at a glance (operator 2026-08-19: "I can't see if anything uploaded"). */
.dropzone.has-file .dropzone__prompt{display:none}
/* Upload status was tiny muted .hint text — easy to miss mid-upload on a phone. Make it a bold
   block; failures read in the stop colour. */
.js-doc-state:not(:empty){display:block;margin-top:var(--sp-2);font-style:normal;font-weight:600;
  color:var(--go);font-size:13px}
.js-doc-state.dz-err{color:var(--stop)}

/* ══ #2 per-item decline rows + inline field errors (2026-08-19, operator walkthrough fixes) ══ */
/* BUG 2: the pair is the unit — a field and ITS "I don't have this" bound together, one flex line,
   checkbox beside its own words, tucked under the input it governs. Generic label/input rules gave
   the checkbox touch-height block layout, which is why it floated away from its label. */
.decl-pair{margin:0 0 var(--sp-3)}
.decl-pair>label{margin-bottom:2px}
.decl-opt{display:flex;align-items:center;gap:6px;margin:2px 0 0 2px;
	font-weight:400;font-size:12px;color:var(--muted);cursor:pointer}
.decl-opt input[type=checkbox]{min-height:0;height:auto;width:auto;margin:0;padding:0;flex:none}
.decl-opt .decl-when{color:var(--muted)}
/* ── TEAM (operator, 2026-09-14): a ROLE IS ONE ROW. The first cut reused .decl-opt, whose input rule covers
   only [type=checkbox]; the radios inherited the generic input rule (touch height, 100% width, padding) and
   rendered as bare circles floating mid-form with their words pushed into a second column. This block owns
   both radio and checkbox: 16px control on the left, bold name + description stacked on the right, the whole
   list flush with the fields above it (no inherited label margin, no 2px indent). */
.role-list{display:flex;flex-direction:column;gap:var(--sp-2);margin:2px 0 var(--sp-2)}
.role-opt{display:grid;grid-template-columns:16px 1fr;column-gap:var(--sp-2);align-items:start;
	margin:0;padding:0;font-weight:400;font-size:13px;color:var(--ink);cursor:pointer;text-align:left}
.role-opt input[type=radio],.role-opt input[type=checkbox]{appearance:auto;-webkit-appearance:auto;
	min-height:0;height:16px;width:16px;margin:2px 0 0;padding:0;border:0;background:none;box-shadow:none}
.role-opt>span{display:block;min-width:0}
.role-opt strong{display:block;font-weight:650;line-height:1.3}
.role-opt .muted{display:block;font-size:12px;line-height:1.35}
.role-opt.is-off{color:var(--muted);cursor:default}
/* Inline field error — rendered AT the offending field; the banner repeats it but names nothing. */
.field-err{display:block;font-weight:600;font-size:12px;color:var(--stop);margin:2px 0 var(--sp-1)}
/* Prompt ≠ failure (BUG 4, 2026-08-19): the address guard ASKS for provenance — amber, at the
   field, with manual entry offered inside the message. Only real validation errors are red. */
.field-prompt{display:block;font-weight:400;font-size:12px;border:1px solid #d9a441;background:#fdf6e7;
	color:#6b4e0e;border-radius:var(--r);padding:var(--sp-2);margin:2px 0 var(--sp-2)}
.field-prompt div{margin-top:var(--sp-1)}
/* BUG 6 guard note: guidance, not an error — amber like the prompt family. */
.decl-note{display:block;font-size:12px;font-weight:600;color:#6b4e0e;background:#fdf6e7;
	border:1px solid #d9a441;border-radius:var(--r);padding:var(--sp-1) var(--sp-2);margin:2px 0 var(--sp-2)}

/* ══ The substitute-W-9 FACE (2026-08-20, walkthrough finding 5): the page reads as the DOCUMENT
   a vendor already knows — monochrome, thin borders, numbered line boxes, Part bands — not a web
   app. Desktop = the recognizable form grid; mobile = the same boxes stacked (a rendered form may
   reflow — ruled). IRS wording is verbatim (see the view header); .w9-ours marks OUR guidance. */
.w9face{border:2px solid var(--ink,#1a1d21);background:var(--bg)}   /* uncapped with the container (walkthrough round 2) */
.w9-head{display:grid;grid-template-columns:auto 1fr auto;border-bottom:2px solid currentColor}
.w9-head>div{padding:var(--sp-2);border-right:1px solid currentColor}
.w9-head>div:last-child{border-right:0}
.w9-head__w9{font-size:26px}
.w9-head__title h2{margin:0 0 4px;font-size:17px;text-align:center}
.w9-head__title{text-align:center}
.w9-head__give{max-width:11em;font-weight:700;font-size:12px;align-self:center}
.w9-small{font-size:11px;line-height:1.35}
.w9-sub{margin-top:4px}
.w9-line{position:relative;border-bottom:1px solid currentColor;padding:var(--sp-1) var(--sp-2) var(--sp-2) 34px}
.w9-num{position:absolute;left:10px;top:7px;font-weight:700;font-size:12px}
.w9-lbl{display:block;font-weight:600;font-size:12px}
.w9-lbl input[name],.w9-csz input,.w9-tinbox{width:100%;margin-top:4px}
.w9-boxes{display:flex;flex-wrap:wrap;gap:6px 18px;margin:6px 0;font-weight:400}
.w9-box{display:inline-flex;align-items:center;gap:6px;font-weight:400;font-size:12px;cursor:pointer}
.w9-box input[type=radio],.w9-3b input[type=checkbox]{appearance:none;width:13px;height:13px;min-height:0;flex:none;
	border:1px solid currentColor;border-radius:0;margin:0;padding:0;display:inline-block;position:relative}
.w9-box input[type=radio]:checked::after,.w9-3b input[type=checkbox]:checked::after{content:"✕";position:absolute;
	inset:0;display:flex;align-items:center;justify-content:center;font-size:11px;line-height:1}
.w9-llc{width:4em !important;display:inline-block;margin-left:6px}
.w9-3b{font-weight:400;display:flex;gap:8px;align-items:flex-start}
.w9-3b input{margin-top:2px}
.w9-exempt{display:flex;flex-wrap:wrap;gap:var(--sp-2);font-weight:400}
.w9-exempt label{flex:1;min-width:16em;font-size:12px}
.w9-exempt input{width:6em;display:block;margin-top:4px}
.w9-csz{display:grid;grid-template-columns:2fr 1fr 1fr;gap:var(--sp-1);margin-top:4px}
.w9-part{border-bottom:2px solid currentColor;padding:4px var(--sp-2);font-weight:700;font-size:13px}
.w9-part__tag{display:inline-block;background:var(--ink,#1a1d21);color:var(--bg,#fff);padding:1px 8px;margin-right:8px}
.w9-tin{border-bottom:1px solid currentColor}
.w9-tinrow{display:flex;flex-wrap:wrap;gap:6px 18px;margin:6px 0}
.w9-or{font-style:italic;font-size:12px;align-self:center}
.w9-tinbox{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.18em;max-width:18em}
.w9-cert{padding:var(--sp-1) var(--sp-2) var(--sp-2)}
.w9-cert>p:first-child{font-weight:700;font-size:12px;margin:4px 0}
.w9-cert ol{margin:0 0 var(--sp-1) var(--sp-3);font-size:12px}
.w9-cert ol li{margin-bottom:4px}
.w9-sign{display:flex;gap:var(--sp-2);border-top:2px solid currentColor;border-bottom:1px solid currentColor;
	padding:var(--sp-1) 0;margin:var(--sp-2) 0;align-items:stretch}
.w9-sign__here{font-weight:800;font-size:15px;padding:2px 6px 0 2px}
.w9-sign__line{flex:1}
.w9-sign__labels{display:flex;justify-content:space-between;border-top:1px solid currentColor;
	font-size:11px;padding-top:2px}
/* The typed name IS the signature: it sits ON the IRS signature line, sized to read as one.
   Plain italic serif, no fake-handwriting webfont — the page stays self-contained. */
.w9-sig-input{width:100%;border:0;border-radius:0;background:transparent;outline:0;
	font-family:Georgia,'Times New Roman',serif;font-style:italic;font-size:20px;
	padding:2px 4px 4px;box-shadow:none}
.w9-sig-input:focus{background:rgba(127,127,127,.06)}
.w9-sigboxes{border:1px solid var(--line);border-radius:var(--r);padding:var(--sp-2);margin-top:var(--sp-2)}
.w9-sigbox{display:flex;gap:8px;align-items:flex-start;font-size:12px;font-weight:600;margin:0 0 var(--sp-1)}
/* Same reset .decl-opt needs: the generic input rule's min-height/padding makes a checkbox huge. */
.w9-sigbox input[type=checkbox]{min-height:0;height:auto;width:auto;margin:2px 0 0;padding:0;flex:none}
.w9-ours{font-weight:400;font-size:12px;color:var(--muted);margin:4px 0}
/* Required-field marking (operator, 2026-08-21). Three rules, three jobs:
   .req-star  — the PERSISTENT indicator: a red asterisk on the label, subtle, never an outline.
                A symbol, not a colour, so the meaning survives colour-blindness and print.
   .field-bad — needs-your-action: the red outline appears ONLY on fields a refused submit actually
                named (server-side $bad()), beside the .field-err message naming what's needed.
                The inline script clears it the moment the field is filled.
   Red therefore keeps its one meaning; a fresh form shows quiet asterisks, never a page of red. */
.req-star{color:var(--stop);font-weight:700;padding-left:2px;font-size:12px}
.field-bad:not(:focus-visible){outline:2px solid var(--stop);outline-offset:1px}
div.field-bad,label.field-bad{outline-offset:4px;border-radius:var(--r)}
.w9face .btn-wide{margin:var(--sp-2)}
.w9face+.muted a{font-weight:600}
@media(max-width:700px){
	.w9-head{grid-template-columns:1fr}
	.w9-head>div{border-right:0;border-bottom:1px solid currentColor}
	.w9-head>div:last-child{border-bottom:0}
	.w9-csz{grid-template-columns:1fr}
	.w9-boxes{flex-direction:column;gap:8px}
}

/* ══ Brand pricing panel at ADMIN DENSITY, portal tokens only (operator walk, 2026-08-21) ══════
   The admin's .fl-bpm modal got the SHAPE right — packages left, the brand's menu right, both
   visible while you decide a price. Reproduced here with this stylesheet's own vocabulary; the
   admin sheet is never imported (standing ruling). No new colours, no new control kinds. */
/* The row's action column: the panel link and the opt control, one unit, wrapping on a phone. */
.pt-bacts{display:flex;align-items:center;justify-content:flex-end;gap:var(--sp-1);flex-wrap:wrap}
.pt-bacts .btn{min-width:6.5em;justify-content:center}
/* A jump lands the panel under the sticky page top instead of flush against it. */
.pkg-panel{scroll-margin-top:var(--sp-4)}
.pkg-head{display:flex;align-items:center;gap:var(--sp-2);margin:0 0 var(--sp-2)}
.pkg-head h3{margin:0;font-size:13px;font-weight:650}
/* ══ Brand rows with INLINE panels (operator walkthrough, 2026-08-21) ══════════════════════════
   The panel opens UNDER the row that was clicked instead of being appended at the page bottom,
   and the open row is highlighted so it is obvious which brand you are pricing. Panels are OPEN
   by default and only collapsible once .js-collapsible is added by script — a vendor whose JS
   failed sees everything rather than a page of dead buttons. */
.pt-brand{border-bottom:1px solid var(--line)}
.pt-brand:last-child{border-bottom:0}
.pt-brands.js-collapsible .pt-panel{display:none}
.pt-brands.js-collapsible .pt-brand.is-open .pt-panel{display:block}
.pt-brand.is-open .pt-brow{background:var(--go-bg)}
.pt-panel{padding:0 var(--sp-3) var(--sp-3);background:#fcfdfc;border-top:1px dashed var(--line)}

/* ══ Package rows — three columns, a header, unpriced muted ════════════════════════════════════ */
/* 🔴 ONE GRID, TWO SURFACES (2026-08-22, operator: the pricing card must "match the density of the
   brand pricing table"). .pfields/.pf are the truck Pricing card's rows and they share THIS rule
   with .pkgs/.pkg rather than copying its column template — a parallel copy would drift the moment
   either is tuned, and the whole point is that the money column lines up down the page. */
/* 🔴 THE TABLE IS CAPPED, AND THE CAP IS THE POINT. The row template is
   `minmax(0,1fr) 140px 92px`, so every pixel of extra container width lands on the NAME track and
   nowhere else. When main{} went 1040px -> 1500px (2026-09-01) this table went with it: the rows
   measured 1412px and the name track 1124px, leaving thirteen short labels ("Entrée", "Entrée +
   side") each sitting at the left of a near-empty 1124px track under a full-width rule. The rules
   are deliberate ROW SEPARATORS — there is not an <hr> on the page — but at that width they read
   as rules drawn across the whole section, which is what was reported.
   Capping the CONTAINER (not the name track) shortens the box, the rows and the separators
   together; capping the track alone would leave the border-bottom spanning the full width and fix
   nothing that was actually complained about. Measured in qa/portal-package-table-check.php. */
.pkgs,.pfields{border:1px solid var(--line);border-radius:var(--r);background:var(--card);margin-top:var(--sp-1);max-width:720px}
.pkghead,.pkg,.pfhead,.pf{display:grid;grid-template-columns:minmax(0,1fr) 140px 92px;gap:var(--sp-3);align-items:center}
.pkghead,.pfhead{padding:6px var(--sp-3);border-bottom:1px solid var(--line);background:var(--bg);
  font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);font-weight:700}
.pkg,.pf{padding:8px var(--sp-3);border-bottom:1px solid var(--line)}
.pkg:last-child,.pf:last-child{border-bottom:0}
.pkg.is-off{background:#fcfcfb}
.pkg.is-off .pkgname b{color:var(--muted)}
.pkgname b,.pfname b{font-size:12.5px}
.pkgname span,.pfname span{display:block;color:var(--muted);font-size:11.5px}
.pkg .price,.pf .pfval{position:relative}
.pkg .price input,.pf .pfval input,.pf .pfval select{text-align:right;width:100%}
/* the $ prefix only indents a MONEY field — a mileage box with an 18px gutter and no currency
   sign reads as a broken money field, which is what "no field wider than it needs" is about. */
.pkg .price input,.pf .pfval.money input{padding-left:18px}
.pkg .price .cur,.pf .pfval.money .cur{position:absolute;left:7px;top:7px;color:var(--muted);font-size:12px}
.pkg .price .per,.pf .pfval .per{font-size:10.5px;color:var(--muted);text-align:right;margin-top:2px}
.pf .pfunit{font-size:11px;color:var(--muted)}
/* the row's opt-out sits in the third column, same slot as a package's status */
.pf .notoffer{font-size:11px;margin:0}
.pkg .status{font-size:11px;font-weight:700}
.pkg .status .on{color:var(--go)}
.pkg .status .no{color:var(--muted);font-weight:400}
/* pass C (2026-09-03): the brand-minimum choice inside the package panel — same card width as .pkgs */
.pkg-min{border:1px solid var(--line);border-radius:var(--r);background:var(--card);margin-top:var(--sp-2);padding:var(--sp-2) var(--sp-3);max-width:720px}
.pkg-min__h{margin:0 0 var(--sp-1);font-size:14px}
.pkg-min__opt{display:flex;align-items:center;gap:var(--sp-2);margin:6px 0;font-size:14px}
.pkg-min__opt input[type=radio]{margin:0;flex:0 0 auto}
.pkg-min__amt{display:inline-flex;align-items:center;gap:4px;margin-left:var(--sp-1)}
.pkg-min__amt input{width:120px}
.pkg-min__help{margin:var(--sp-1) 0 0;font-size:13px;line-height:1.45}
.pkg-empty{border:1px dashed var(--line);border-radius:var(--r);padding:var(--sp-4);text-align:center;
  color:var(--muted);background:var(--card);font-size:13px;margin-top:var(--sp-2)}
@media(max-width:560px){
  .pkghead{display:none}
  .pkg{grid-template-columns:1fr;gap:var(--sp-1)}
}

/* ONE COLUMN, ALWAYS — menu first, then the fields (operator ruling 2026-08-21, reversing the
   side-by-side pass). Reading order IS the design here; a two-pane layout at admin density is the
   thing that was rejected. No max-height and no inner scroller: nothing about what a vendor is
   committing to should sit below a fold inside a box. */
.pkg-cols{display:grid;grid-template-columns:1fr;gap:var(--sp-3)}
.pkg-col{min-width:0}
.pkg-menu{border-bottom:1px solid var(--line);padding-bottom:var(--sp-2)}
.pkg-menu__h{margin:0 0 var(--sp-1);font-size:12px;font-weight:650;color:var(--muted)}
.pkg-menu__g{margin:var(--sp-2) 0 var(--sp-1);font-size:12px;font-weight:650}
.pkg-menu__g:first-of-type{margin-top:var(--sp-1)}

/* ══ Pricing field pairs — the field and the one control that disables it, on one line ═════════
   (operator walkthrough 2026-08-21; shape from the mockup, portal tokens only.) The grid caps the
   field column so a label and its input stay together even at the 1040px content width, and puts
   the single "Not offered" radio immediately to its right instead of at the far edge. */
/* .fieldpair is now only the SEMANTIC marker (one control per row) that
   qa/portal-pricing-radios-check.php pins; when it is also a .pf the grid above wins. */
.fieldpair:not(.pf){display:grid;grid-template-columns:minmax(0,300px) auto;gap:var(--sp-3);
  align-items:center;margin-bottom:var(--sp-3)}
.fieldpair:last-of-type{margin-bottom:0}
.fieldpair__f{min-width:0}
.fieldpair label.f{display:block;font-size:12px;font-weight:650;margin-bottom:3px}
.fieldpair label.f input[type=number]{margin-top:3px;width:100%}
.fieldpair .unit{color:var(--muted);font-size:11px;margin-top:3px}
.notoffer{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);
  white-space:nowrap;font-weight:400;cursor:pointer;min-height:0}
.notoffer input[type=radio],.notoffer input[type=checkbox]{min-height:0;height:auto;width:auto;margin:0;padding:0;flex:none}
/* "NOT OFFERED" TICKED (operator ruling 2026-09-03): the row must read unmistakably as "you cannot type
   here" — muted text + muted ground + not-allowed cursor on the rate, and the helper text and unit go
   quiet with it. The input is READONLY (never disabled) so the kept value still posts on save. */
.pf.is-off .pfval input{color:var(--muted);background:var(--bg);cursor:not-allowed;border-color:var(--line)}
.pf.is-off .pfval .cur,.pf.is-off .pfval .per,.pf.is-off .pfname span,.pf.is-off .pfunit{color:#a3a8b0}
.pf.is-off .pfname b{color:var(--muted)}
/* THE BRAND PRICING REFERENCE on the truck panel's Brands step (operator ruling 2026-09-04): read-only,
   one line, quiet — the truck ASSIGNS a brand; prices and the minimum are set once on /brands. Deliberately
   NOT a table: a table here would re-invite the per-truck pricing habit this ruling removed. */
.brand-ref{margin:3px 0 0;font-size:11.5px;color:var(--muted);line-height:1.45}
.brand-ref a{color:var(--accent)}

/* THE PRICING CHANGE LOG (operator ruling 2026-09-03): a collapsed <details> card — findable, not loud. */
.phist>summary{cursor:pointer;font-size:12.5px;color:var(--muted);list-style:none}
.phist>summary::before{content:"▸ ";color:var(--muted)}
.phist[open]>summary::before{content:"▾ "}
.phist__t{width:100%;border-collapse:collapse;margin-top:var(--sp-2);font-size:12px}
.phist__t th{text-align:left;color:var(--muted);font-weight:500;border-bottom:1px solid var(--line);padding:4px 6px}
.phist__t td{padding:5px 6px;border-bottom:1px solid var(--line);vertical-align:top}
.phist__theirs td:nth-child(2){color:var(--accent);font-weight:600}
.phist__mine td:nth-child(2){color:var(--muted)}
.phist__old{color:var(--muted)}
.phist__new{font-weight:600}
@media(max-width:560px){
  .fieldpair:not(.pf){grid-template-columns:1fr;gap:var(--sp-1)}
  /* phones: the three-column grid becomes label / field, with the unit tucked under the field —
     140px + 92px beside a label does not fit at 390px. */
  .pkghead,.pfhead{display:none}
  .pkg,.pf{grid-template-columns:minmax(0,1fr) 132px;row-gap:var(--sp-1)}
  .pf .pfunit,.pf .notoffer,.pkg .status{grid-column:2;justify-self:end;text-align:right}
  .notoffer{padding-left:2px}
}

/* ══ Pricing: bind each control to the field it governs (operator walk, 2026-08-21) ════════════
   CSS-ONLY BY DESIGN. The markup posts `extra_hours_offered` / `ayce_extra_hours_offered` as a
   two-member radio group — value=1 owns the rate field beside it, value=0 is "Not offered". Both
   are load-bearing; NEITHER is orphaned. What was wrong was purely presentation: .row's flex put
   the "yes" radio at the far left of a wide .grow label (reading as decoration on the number
   input) and stranded "Not offered" against the right edge, so nothing on screen said the two
   were one choice. Identical defect to the /business "I don't have this" checkbox, and this is
   that fix — the opt-out drops onto its own line directly beneath the field it governs, tucked
   and muted, in .decl-opt's exact vocabulary.
   🔴 Kept to CSS deliberately: no markup change means the POSTED CONTRACT is byte-identical, so
   portal-refusal-preserves-input-check still means what it meant before the change. */
.price-form .row:has(input[type=radio]){flex-wrap:wrap;row-gap:2px}
.price-form .row:has(input[type=radio])>label:last-child{flex-basis:100%;display:flex;align-items:center;
  gap:6px;margin:2px 0 0 2px;font-weight:400;font-size:12px;color:var(--muted);cursor:pointer;min-height:0}
.price-form .row:has(input[type=radio])>label:last-child input[type=radio]{min-height:0;height:auto;
  width:auto;margin:0;padding:0;flex:none}
/* The commission choice was a BARE <label> in a card, so generic label rules gave it touch-height
   block layout — the same reason .decl-opt had to exist. Same treatment, same result. */
.price-form .card>label:has(>input[type=checkbox]){display:flex;align-items:center;gap:6px;
  font-weight:400;min-height:0}
.price-form .card>label:has(>input[type=checkbox])>input[type=checkbox]{min-height:0;height:auto;
  width:auto;margin:0;padding:0;flex:none}

/* ══ Your own brands (item 13) + the forward exit (item 12) — 2026-08-21 ═══════════════════════ */
.pt-own .own{display:flex;align-items:center;gap:var(--sp-2);padding:var(--sp-2) var(--sp-3);
  border-bottom:1px solid var(--line);margin:0}
.pt-own .own:last-child{border-bottom:0}
.nextbar{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap;margin-top:var(--sp-4);
  padding:var(--sp-3);background:var(--card);border:1px solid var(--line);border-radius:var(--r)}

/* ══ Walk fixes 4–7 (2026-08-21) ═══════════════════════════════════════════════════════════════ */
/* item 7 — address parts sized to what they hold, not to flex leftovers. City was ~30px. */
.addr-row{display:grid;grid-template-columns:minmax(0,2.4fr) 5.5em 7em;gap:var(--sp-2);align-items:end}
.addr-row label{margin:0}
.addr-row input{width:100%}
@media(max-width:560px){.addr-row{grid-template-columns:1fr 1fr;gap:var(--sp-1)}
  .addr-row label:first-child{grid-column:1 / -1}}
/* item 6 — the pay rule reads BEFORE the field, not as a footnote after it. */
.rule-note{background:var(--go-bg);border-left:3px solid var(--go);border-radius:var(--r);
  padding:var(--sp-2) var(--sp-3);margin:0 0 var(--sp-2);font-size:12.5px;line-height:1.45}
/* item 4 — the save says so where the save happened. */
.pkg-saved{margin:var(--sp-2) 0 0;padding:var(--sp-1) var(--sp-2);border-radius:var(--r);
  background:var(--go-bg);border:1px solid var(--go);color:var(--go);font-weight:650;font-size:12.5px}
/* item 5 — a priced brand's control is a disclosure, not a fresh action. */
.pt-disclose{font-weight:600}
.pt-caret{display:inline-block;width:.8em;color:var(--muted)}

/* ══ Walk round 2 (2026-08-21) — withdrawal made discoverable, and finishing puts a brand away ══ */
/* The Remove control is quiet: withdrawal is a normal act, not a destructive one, and it must not
   compete with the one primary save. Hidden on unpriced rows — there is nothing to remove. */
.pkg-remove{background:none;border:0;padding:0;margin-left:var(--sp-1);font:inherit;font-size:11px;
  color:var(--muted);text-decoration:underline;text-underline-offset:2px;cursor:pointer}
.pkg-remove:hover{color:var(--stop)}
.pkg.is-off .pkg-remove{display:none}
/* The ✓ rides the collapsed brand row so a save is visible without reopening the panel. */
.pt-brow .pkg-saved{margin:0 var(--sp-1) 0 0;padding:1px var(--sp-1);font-size:11px;border-radius:20px;
  background:var(--go-bg);border:1px solid var(--go);color:var(--go);font-weight:700;white-space:nowrap}

/* ── the Business-info gap list (2026-08-22) ─────────────────────────────────────────────────
   Sits directly under the .fixline banner and reads as its detail, not as a new component: the
   banner states the condition, this states which items. Indented to the banner's text, not to the
   page, so the two read as one block. */
.onb-gaps{margin:calc(var(--sp-2) * -1) 0 var(--sp-3);padding-left:calc(14px + 1.1em);color:var(--ink)}
.onb-gaps li{margin:2px 0;line-height:1.45}

/* REPORT A PROBLEM link under the drawer foot (operator ruling 2026-09-03): quiet, one line, every page. */
.side-help{margin:0;padding:var(--sp-1) var(--sp-3) var(--sp-2);font-size:.85em}
.side-help a{color:var(--muted);text-decoration:underline}

/* ── PHOTOS (v205, 2026-09-03): the admin Media panel's drop zone on the portal's own tokens — same shape, no new colour.
   Upload for review is .btn-go (operator 2026-09-03: "green, matching the admin's primary action"), which is a deliberate
   exception to the 2026-08-20 vocabulary line above (solid blue = an UPLOAD): on this page the upload IS the one primary
   action, and the operator ruled it green. ── */
.dropzone{border:2px dashed var(--line);border-radius:var(--r);padding:var(--sp-3);text-align:center;background:var(--bg);transition:border-color .12s,background .12s}
.dropzone.is-over{border-color:var(--accent);background:var(--accent-bg)}
.dropzone p{margin:4px 0}
.dz-btns{display:flex;gap:var(--sp-1);justify-content:center;flex-wrap:wrap;margin:var(--sp-1) 0}
.dz-count{min-height:1em;margin:var(--sp-1) 0 4px;font-size:14px;font-weight:700;color:var(--muted)}
.dz-count.is-active{color:var(--go-ink)}
.dz-previews{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin:4px 0 2px}
.dz-previews img{width:56px;height:56px;object-fit:cover;border-radius:6px;border:1px solid var(--line)}
.photogrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:var(--sp-2);margin:var(--sp-2) 0}
.photogrid figure{margin:0}
.photogrid img{width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:6px;border:1px solid var(--line)}
.photogrid img.is-dim{opacity:.6}
@media(pointer:coarse){.dz-drag{display:none}}
/* per-file progress rows (the admin uploader's .up-row on portal tokens) */
.uploader__progress{list-style:none;margin:var(--sp-1) 0 0;padding:0;display:flex;flex-direction:column;gap:4px;max-height:220px;overflow:auto}
.up-row{display:flex;justify-content:space-between;gap:var(--sp-2);font-size:13px;padding:4px 8px;border-radius:6px;background:var(--bg)}
.up-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.up-status{color:var(--muted);white-space:nowrap}
.up-status.is-ok{color:var(--go-ink)} .up-status.is-err{color:var(--stop)} .up-status.is-pending{color:var(--muted)}

/* ══ THE VENDOR BRAND WIZARD (operator order 2026-09-06) — item CARDS, the logo tile, locked package rows ══
   Composes the existing vocabulary (.card, .pkgs/.pkg, .pfields, .dropzone, .chip, .photogrid, .bcard__tile);
   these rules only add the shapes the wizard has and nothing else does. No full-width rows: items are cards
   on the same auto-fill grid the brand cards use. */
.bw-items{display:grid;gap:var(--sp-2);grid-template-columns:repeat(auto-fill,minmax(270px,1fr));align-items:stretch}
.bw-item{display:flex;flex-direction:column;gap:var(--sp-1);border:1px solid var(--line);border-radius:var(--r);background:var(--card);padding:var(--sp-2) var(--sp-3);min-width:0}
.bw-item.is-removed{display:none}
.bw-item label{margin:2px 0}
.bw-item textarea{width:100%;font:inherit;font-size:13px;min-height:3.4em;padding:6px var(--sp-2);border:1px solid var(--line);border-radius:var(--r);background:var(--card);color:var(--ink);resize:vertical}
/* items 4+5 (2026-09-08): tile · text · control as a GRID. The old flex row gave the status text no flex rule, so the mini
   upload form took its natural width and the text collapsed to one word per line. The tile is the logo's .bcard__tile. */
.bw-photo{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:var(--sp-1) var(--sp-2);margin-top:auto;min-width:0}
/* the upload control takes its OWN row under tile + text (measured 2026-09-08: three columns in a 274px card left the text 61px wide) */
.bw-photo .bw-mini-upload{grid-column:1 / -1;justify-self:start}
.bw-photo .bw-item-tile{width:56px;height:56px;flex-basis:56px}
.bw-photo .bw-item-tile img{max-width:48px;max-height:48px;object-fit:cover;border-radius:4px}
.bw-photo .bw-item-tile img.is-dim{opacity:.6}
.bw-photo-text{min-width:0;line-height:1.35;overflow-wrap:anywhere}
.bw-photo .bw-thumb{width:56px;height:56px;flex:0 0 56px;border-radius:6px;border:1px solid var(--line);background:var(--off)}
/* the one-file upload slot: a button-sized dropzone, no prompt copy, uploads on pick (data-auto) */
/* the item slot's upload form — EVERY state (2026-09-08): empty · uploading · just uploaded · settled · replaced. It sits on its own
   row under tile + text (grid-column below), never wider than the card: the dropzone is a compact inline control, the progress
   rows and the status line wrap inside the card (a phone photo's filename is long), nothing sets a width of its own. */
.bw-mini-upload{margin:0;flex:0 0 auto;min-width:0;max-width:100%}
.bw-mini-upload .dropzone{display:inline-flex;align-items:center;padding:2px 4px;margin:0;gap:2px;border-width:1px;background:transparent;min-width:0;width:auto;text-align:left}
.bw-mini-upload .dz-btns{margin:0}
.bw-mini-upload .dz-count,.bw-mini-upload .dz-previews{display:none}
.bw-mini-upload .uploader__progress{list-style:none;margin:4px 0 0;padding:0;font-size:11px;min-width:0;max-width:100%}
.bw-mini-upload .uploader__progress li{display:flex;flex-wrap:wrap;gap:0 6px;min-width:0;white-space:normal;overflow-wrap:anywhere;min-height:22px;line-height:22px}
.bw-mini-upload .up-name{min-width:0;max-width:100%;overflow-wrap:anywhere}
.bw-mini-upload .photo-status{margin:4px 0 0;min-width:0;overflow-wrap:anywhere;white-space:normal}
.bw-logo{display:flex;align-items:center;gap:var(--sp-3)}
.bw-logo .bcard__tile{width:64px;height:64px;flex-basis:64px}
.bw-logo .bcard__tile img{max-width:56px;max-height:56px}
.bw-logo .bcard__tile img.is-dim{opacity:.6}
.bw-logo-actions{gap:var(--sp-1);margin:0 0 var(--sp-1)}
/* THREE REQUIRED PACKAGES (2026-09-08): group headings, the Required / Optional marks, the parts-sum hint */
.pkg-group+.pkg-group{margin-top:var(--sp-3)}
.pkgname .pkg-tag,.pkg-tag{display:inline-block;width:auto;font-size:11px;font-weight:650;letter-spacing:.02em;text-transform:uppercase;padding:1px 7px;border-radius:999px;vertical-align:middle;margin:2px 0 0 4px;line-height:1.5}   /* .pkgname span is display:block full width — the tag must stay a pill */
.pkg-tag--req{background:var(--stop-bg);color:var(--stop);border:1px solid var(--stop)}
.pkg-tag.muted{border:1px solid var(--line)}
.pkg.is-required .pkgname b{text-decoration:underline;text-decoration-color:var(--stop);text-underline-offset:3px}
.pkgname [data-pkg-hint]{display:block;margin-top:2px;color:var(--go-ink,var(--go))}
.pkgname [data-pkg-ayce-note]{display:block;margin-top:2px}
.retired-brands summary{cursor:pointer;list-style:revert}
.retired-brands[open] summary{margin-bottom:var(--sp-1)}
[data-brand-create] .field-err{display:block;margin-top:4px}
/* item C (2026-09-08): a progress row is one fixed line, so a reserved min-height (rows × 22px, set by photo-form.js when the
   batch starts) holds the gallery still while uploads run */
.bw-food-upload .uploader__progress li,.bw-logo-upload .uploader__progress li{min-height:22px;line-height:22px}
.bw-cats{display:flex;flex-wrap:wrap;gap:6px 12px}
.bw-cats label{display:inline-flex;align-items:center;gap:5px;font-weight:500;margin:0;min-height:var(--touch)}
.bw-cats input[type=checkbox]{width:auto;min-height:0;margin:0}
.pkg.is-locked{background:#fcfcfb}
.pkg.is-locked .pkgname b{color:var(--muted)}
.pkg.is-locked .pkgname span{color:var(--warn)}
.pkg .bw-confirm{display:block;font-size:11px;font-weight:500;margin:4px 0 0;text-align:right;color:var(--warn)}   /* amber (2026-09-11): the flag, not a refusal */
.pkg .bw-confirm input{width:auto;min-height:0;margin:0 3px 0 0}
/* the refusal at the field (2026-09-11): under the price box it is about, right-aligned like the confirm row; and beside Save */
.pkg .price .field-err{text-align:right;margin:4px 0 0}
.pkg-err-near{margin:var(--sp-2) 0 0}
.brand-wizard .bw-items .bw-item{background:var(--card)}

/* ── PAPERWORK: three headed boxes (operator ruling 2026-09-06) ───────────────────────────────────
   🔴 EVERY DOCUMENT TYPE IS A BOX WITH A HEADING. Before this, one "Certificate" label sat above a
   lone uploader and the other two types lived as <option> values inside a collapsed <details> — a
   vendor could not tell what they were uploading or where the other two went. The heading is the
   slot label; the box is the boundary that stops a fire inspection landing in the health slot. */
.docbox{border:1px solid var(--line);border-radius:12px;background:var(--card);
  padding:var(--sp-3);margin:0 0 var(--sp-2)}
.docbox__h{display:flex;align-items:center;flex-wrap:wrap;gap:8px;
  margin:0 0 4px;font-size:14px;font-weight:650}
.docbox > .hint{margin:0 0 var(--sp-2)}
.docbox .truckline{margin:var(--sp-2) 0 4px}
.docbox .doc-hist{margin:0 0 var(--sp-2)}
.docrow{display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap}
.docrow__file{flex:2 1 220px;min-width:0}
.docrow__exp{flex:1 1 140px;min-width:0}
.docrow__st{flex:0 0 5.5em}
.docrow__st input{width:100%;text-transform:uppercase}

/* 🔴 A CONTROL THAT REVEALS IS STILL A CONTROL (operator ruling 2026-09-06, second instance —
   same defect as the Remove button an hour earlier). "Add a license for this truck" was a grey
   <summary> reading as caption text, so the only route to a second state was invisible. A <summary>
   styled as a button keeps the whole disclosure JS-free while looking like the action it is; the
   marker rules are what stop it rendering a stray triangle inside the button. */
.addmore{margin-top:var(--sp-1)}
.addmore > summary{cursor:pointer;list-style:none;width:auto}
.addmore > summary::-webkit-details-marker{display:none}
.addmore > summary::marker{content:""}
.addmore[open] > summary{margin-bottom:var(--sp-2)}

/* Event-minimum lists: two headed lines, each on its own row (operator ruling 2026-09-06). Running
   them together in one sentence hid which list the number does NOT govern. */
.minline{display:block;margin-top:3px}

/* ── THE PANEL'S STEP STRIP YIELDS TO THE PAGE'S FLOW STRIP (operator ruling 2026-09-07) ──────────
   /trucks carries both: the unconditional five-step "Getting set up" flow bar, and — once a truck
   panel is open — that truck's own six-step strip. They compose the same classes deliberately, which
   is what made two of them stacked read as one broken control.
   🔴 THE PANEL'S ONE YIELDS, AND ONLY IN APPEARANCE. Its links are the only way back to an earlier
   truck step (forward is served by Save-and-continue), so removing it would trade a visual complaint
   for a navigational dead end. It keeps every link and loses the card chrome: no border, no fill,
   tighter, so it reads as step navigation inside the panel rather than a second banner.
   🔴 CSS ONLY, NO MARKUP CHANGE — `.truck-steps` already distinguishes it, and qa/truck-panel-check.php
   matches the exact string `<div class="onb-strip truck-steps"` in three places. Adding or reordering
   a class there would have broken another seat's harness to achieve nothing this rule needs. */
.onb-strip.truck-steps{border:0;background:transparent;padding:2px 0;margin:0 0 var(--sp-2);
  font-size:.82em;border-bottom:1px solid var(--line);border-radius:0}
.onb-strip.truck-steps .onb-strip__t{opacity:.75}

/* A blocking badge is actionable — it names a lapsed document and links to where it is replaced
   (operator ruling 2026-09-08). Only blocking badges get this; an ordinary state badge stays inert,
   so the affordance means something when it appears. */
.bbadge--act{text-decoration:none;cursor:pointer}
.bbadge--act:hover{text-decoration:underline}

/* What is still outstanding, in words (operator ruling 2026-09-08) — the card's line and the panel's
   line under the step strip. Quiet: it is orientation, not an alarm; the badge already carries state.
   The off-screen variant is distinguished only by its label ("Waiting on Brands"), deliberately — it
   is not a worse state than the others, just one that lives on a different screen. */
.bcard__left,.steps-left{margin:2px 0 0;font-size:.86em;color:var(--muted);line-height:1.4}
.bcard__waiting,.bcard__withus,.steps-left--away{margin:2px 0 0;font-size:.86em;color:var(--muted);line-height:1.4}
.bcard__left a,.steps-left a,.bcard__waiting a,.steps-left--away a{color:inherit;text-decoration:underline}

/* The commission breakdown (operator ruling 2026-09-08): label left, amount right, so a vendor can add
   the column up. Tabular figures keep the decimal points aligned — the whole point is that the
   arithmetic is checkable at a glance. */
.pl-row{display:flex;justify-content:space-between;gap:var(--sp-2);line-height:1.5}
.pl-amt{font-variant-numeric:tabular-nums;white-space:nowrap}
.pl-row--total{border-top:1px solid var(--line);margin-top:3px;padding-top:3px}

/* The business-level blocker: full width, above the page's cards, so its SCOPE is legible from
   position alone — it is not any truck's state (operator ruling 2026-09-08). Red is kept: it is a
   real blocker, and the ruling was about placement and voice, not about softening it. */
.biz-block{margin-bottom:var(--sp-3)}

/* ── /paperwork (operator ruling 2026-09-08; qa/PROPOSAL-2026-09-08-paperwork-page-restructure.md) ──
   Business liability FIRST in its own full-width block, a summary of what is outstanding under it, then
   ONE column of truck rows ordered by urgency — a collapsed <details> per truck carrying a status chip.
   No tabs. The two-column .cardgrid of four equal cards is gone from this page.
   🔴 COLOUR RULE: amber ONLY on "Expiring soon" (a real prompt; the word was "Renewal due" until round 3). "Awaiting verification" and "Couldn't
   check" wear the .waitline treatment (page ground + plain rule) — waiting on us is a status, not a
   warning (the 2026-09-08 ruling applied to the new element). Red pair = Needs documents and
   Not accepted. Every colour is an existing token; no new hex.
   🔴 ROOM: the business heading and row names sit at --fs-1 (16px) instead of the card's 13px, chips at
   12.5px instead of 11px, hints at 13px. Inside an OPEN row the three document boxes go 3-ACROSS at
   ≥1100px — which is also what keeps every .docbox hint inside the 130 c/l ceiling
   qa/portal-prose-measure-check.php enforces (a full-width hint would run ~170 chars/line). */
.pw-business{margin:var(--sp-3) 0 var(--sp-4)}
.pw-liab{padding:var(--sp-3) var(--sp-4)}
.pw-liab h2{font-size:var(--fs-1);display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2);margin:0 0 var(--sp-2)}
.pw-liab h2 .chip{font-size:12.5px;padding:3px 10px;border-radius:999px}
.pw-fleet{margin:0 0 var(--sp-2);font-size:13.5px;color:var(--muted)}
.pw-fleet--stop{color:var(--stop-ink);font-weight:650}
.pw-docline{margin:0 0 var(--sp-2);font-size:14px;line-height:1.45}
.pw-docline--absent{color:var(--stop-ink);font-weight:600}
/* ── THE COLLAPSED ROW'S BREAKDOWN (operator ruling 2026-09-09) ──────────────────────────────────
   🔴 THE PILL ROW THAT STOOD HERE IS DELETED, CSS AND ALL. Four .pill rules and .pw-pills went with it;
   they are not kept "in case". Orphaned CSS for a deleted surface is how a deleted surface comes back.
   The pills were a second derivation of what the rows already knew and two of them counted the same
   three documents; the row below now carries the whole breakdown and nothing sits above it.
   ⚠️ The .pill--ok/warn/bad values were byte-copies of the admin dashboard's palette (app.css) kept in
   sync by hand, because the two dashboards ship separate stylesheets to separate hosts and share nothing
   at runtime. That obligation is GONE with them. If a pill is ever needed here again, copy them from
   app.css again — do not re-derive them from these tokens, which are the portal's, not the admin's.

   🔴 THE GROUP LABEL IS THE OPEN CARD'S HEADING, IN THE SAME TYPE. .prow__lbl deliberately wears
   .docgroup's treatment — 13px, bold, uppercase, letter-spaced, muted — because they are the same words
   for the same group (PaperworkRows::GROUP_LABEL is character-identical to $__headings in
   _truck_paperwork.php). A vendor who reads "YOU NEED TO UPLOAD" on the closed row and opens the card
   must land on the same phrase in the same voice, or the two readouts read as two subjects.
   🔴 ONLY THE LABEL CARRIES A TONE, AND THE ROW'S GROUND NEVER DOES. The chip on the right is the card's
   state; three coloured bands down the summary would compete with it and with the .docbox edges inside. */
.pw-h{font-size:var(--fs-1);font-weight:650;margin:0 0 var(--sp-2)}
.pw-empty{padding:var(--sp-3) var(--sp-4)}
.pw-empty h2{font-size:var(--fs-1)}
.pw-empty p{font-size:14px;line-height:1.5;margin:var(--sp-1) 0 var(--sp-3)}
/* 🔴 A CARD, IN THE PORTAL CARD LANGUAGE (operator ruling 2026-09-08 item 4). The geometry, radius and
   badge slot are .bcard/.bbadge's, so /paperwork, /brands and /trucks read as one system. It stays a
   <details> because several cards must be open at once (see the view's note): the panel-host pattern the
   other two pages use allows exactly ONE open panel, and this page is ruled open-by-default PER CARD.
   The stack is SINGLE-COLUMN on purpose — an expanding card inside a multi-column grid reflows its whole
   row, which is the defect the /brands panel-host ruling exists to prevent. */
.pw-cards{display:grid;gap:var(--sp-2);grid-template-columns:1fr}
.prow{padding:0;margin:0;border-radius:12px}
.prow__sum{display:flex;flex-direction:column;gap:4px;min-height:48px;justify-content:center;
  padding:var(--sp-3);cursor:pointer;list-style:none}
.prow__sum::-webkit-details-marker{display:none}
.prow__sum::marker{content:""}
.prow__top{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--sp-2)}
/* the disclosure marker rides the NAME row, so the card top keeps .bcard's name-left / badge-right shape */
.prow__top::before{content:"▸";color:var(--muted);flex:0 0 1em;line-height:1.3}
.prow[open]>.prow__sum>.prow__top::before{content:"▾"}
.prow__name{font-size:var(--fs-1);font-weight:650;flex:1 1 auto;min-width:0;line-height:1.3}
/* a BADGE, not a button — .bbadge's pill shape, and it WRAPS: a nowrap badge ran past the card edge on
   /trucks on 2026-09-08 and the fix there was the same one. */
.prow__chip{display:inline-block;padding:3px 10px;border-radius:999px;font-size:12.5px;font-weight:650;line-height:1.3;white-space:normal;max-width:100%;overflow-wrap:anywhere;text-align:right}
.prow__chip--stop{background:var(--stop-bg-strong);color:var(--stop-ink)}
.prow__chip--warn{background:var(--warn-bg-strong);color:var(--warn-ink)}
.prow__chip--go{background:var(--go-bg-strong);color:var(--go-ink)}
.prow__chip--wait{background:var(--bg);color:var(--muted);border:1px solid var(--line)}
.prow__why{color:var(--muted);font-size:13.5px;min-width:0;line-height:1.4;padding-left:1.5em}
.prow__grp{display:block;color:var(--muted);font-size:13.5px;min-width:0;line-height:1.45;padding-left:1.5em;overflow-wrap:anywhere}
.prow__lbl{font-size:12px;font-weight:700;letter-spacing:.03em;text-transform:uppercase}
.prow__grp--needs .prow__lbl,.prow__grp--not_accepted .prow__lbl{color:var(--stop)}
.prow__grp--renewal .prow__lbl{color:var(--warn)}
.prow__grp--awaiting .prow__lbl{color:var(--muted)}
/* the account-level blocker on a truck card: grey, stated, linked — never the card's own red (operator) */
.prow__blocked{display:block;color:var(--muted);font-size:13px;line-height:1.45;padding-left:1.5em;margin-top:2px}
.prow__blocked a{color:var(--muted)}
/* "Can't take bookings" beside the business NAME — .bbadge's shape and the card badges' --stop pair, so
   it reads with the badges on /brands and /trucks rather than inventing a third marker (operator). */
.pwbadge{display:inline-block;padding:3px 10px;border-radius:999px;font-size:12.5px;font-weight:650;
  line-height:1.3;white-space:normal;max-width:100%;overflow-wrap:anywhere;vertical-align:middle}
.pwbadge--stop{background:var(--stop-bg-strong);color:var(--stop-ink)}
.prow__body{display:grid;gap:var(--sp-2);grid-template-columns:1fr;
  padding:var(--sp-2) var(--sp-3) var(--sp-3);border-top:1px solid var(--line)}
@media(min-width:1100px){.prow__body{grid-template-columns:repeat(3,minmax(0,1fr))}}
.prow__body > .docbox{margin:0}
.prow__body .docbox__h{font-size:14px}
.prow__body .hint{font-size:13px}
/* the partial's state banners (wait/ok/fix/due) come BEFORE its three boxes; in the 3-across grid they
   must span the row, or the first banner takes a cell and the fire box wraps under (measured 2026-09-08) */
.prow__body > .waitline,.prow__body > .okline,.prow__body > .fixline,.prow__body > .dueline{grid-column:1/-1;margin:0}
/* 🔴 THE TWO GROUP HEADINGS INSIDE AN OPEN CARD (operator ruling 2026-09-08 round 3) — they replace two
   banners that named no document, so they must read as LABELS over the boxes they head, not as another
   banner. No ground, no border, no colour: the boxes underneath carry the state. Spanning the grid keeps
   each group's boxes on their own rows at ≥1100px, which is the whole point of grouping them. */
.docgroup{grid-column:1/-1;margin:var(--sp-2) 0 0;font-size:13px;font-weight:700;
  letter-spacing:.03em;text-transform:uppercase;color:var(--muted)}
.prow__body > .docgroup:first-child{margin-top:0}

/* ── GROUP WEIGHT (operator ruling 2026-09-08 round 3, item 2) ───────────────────────────────────
   🔴 THE TEST IS "TELL WHICH HALF IS MINE FROM ACROSS THE ROOM, WITHOUT READING THE HEADING", so the
   signal is carried by the BOXES, not the label above them. Before this, both groups were identical
   white cards and the only difference was 13px grey capitals — which fails that test by construction.
   ACTION half: the normal card surface, full contrast, a 4px left edge, buttons unchanged. The edge's
   COLOUR is the document's own, not the group's — see THE LEFT EDGE below.
   PARKED halves (with-us AND the unlabelled current group): the page's own ground, muted ink, a dim.
   🔴 Dimming is the only signal that survives at distance without reading, and the portal already owns
   the gesture (.bgrid--dimmed on /brands when a panel opens). Colour alone would ask the vendor to tell
   white from off-white, measured elsewhere in this file at 255 vs 246 — "technically distinct and
   visually nothing".
   🔴 THE OPACITY WAS CHOSEN BY THE TEST, NOT BY EYE. qa/paperwork-group-weight-check.php composites the
   cumulative ancestor opacity into BOTH the text colour and its background before taking the ratio —
   an ordinary contrast probe reads getComputedStyle().color, which `opacity` does not change, so it
   would have passed while the rendering darkened. The value below is the strongest dim that kept every
   painted node at or above 4.5:1 in that probe's sweep. Do not hand-tune it; re-run the sweep.
   🔴 A QUIET BUTTON IS STILL A BUTTON (operator): a vendor may renew early or replace a document we are
   already checking. The control is recessive, never absent. */
.docbox--upload{background:var(--card)}
/* ── THE LEFT EDGE FOLLOWS THE DOCUMENT, NOT THE GROUP (operator ruling 2026-09-08 round 4) ────────
   🔴 THIS RULE USED TO BE `border-left:2px solid var(--stop)` ON .docbox--upload, which painted every box
   under "You need to upload" red — including a document that is only EXPIRING SOON. Measured on truck
   1000408: an auto certificate and a health permit, both expiring, both red, both sitting next to their
   own amber "expiring soon" chip. The estate colour rule is RED = expired or missing, AMBER = expiring
   soon, and a group is not a document state: the group says whose move it is, the edge says what state
   the document is in.
   🔴 THE TONE IS CHOSEN IN PaperworkRows::docChip, IN THE SAME TABLE ROW AS THE CHIP, and the view writes
   `.docbox--edge-<tone>` beside the group class. That is what makes the edge and the chip on one box
   unable to disagree — one row per state, both readouts read out of it. Do not add an edge rule keyed on a
   group, a heading or anything else that is not that row.
   🔴 ONE HUE FAMILY PER STATE, NO THIRD PALETTE: red is --stop, the ink .chip-booked already wears.
   ⚠️ THE YELLOW IS A ROLE TOKEN, NOT A NEW COLOUR (round 5): --warn is an INK and stays one; a bar needs
   saturation where a word needs contrast, so the bar wears --warn-edge on the same hue — the reasoning,
   the eleven measured candidates and the rejected --warn are all recorded at the token itself. The check
   asserts the two stay within 25° of hue, which is what keeps bar and chip meaning one thing.
   🔴 4px, NOT 2px, AND BOTH STATES TOGETHER (round 5). A street-light yellow cannot be dark, so a 2px
   bar of it is faint on white; width is what buys back the read. Both states widen so the bar stays ONE
   axis — a per-state width would be a second signal nobody asked for.
   🔴 An all-current box gets NO tone and therefore no edge — nothing about its own state is red or
   yellow, and a green edge would be a claim the group heading contradicts.
   ⚠️ "Not on file" chips grey (.chip-off, the ruled grey of "nothing here") and edges RED, by the same
   ruling: "a box in the same group that is expired or not on file stays red". It is the ONE pairing where
   the two hues differ, and it is written on one line in docChip so it cannot be lost. */
.docbox--edge-stop{border-left:4px solid var(--stop)}
.docbox--edge-warn{border-left:4px solid var(--warn-edge)}
/* 🔴 0.65 WAS CHOSEN BY THE TEST, NOT BY EYE (operator instruction, 2026-09-08 round 3). The sweep in
   qa/paperwork-group-weight-check.php walked 0.55-0.80 compositing the dim into every painted node:
   0.60 left the assist control at 4.36:1 (under the floor) and 0.65 cleared it at 5.12:1. It is the
   STRONGEST dim that keeps every node at or above 4.5:1 — re-run the sweep rather than hand-tuning. */
.docbox--quiet,.pquiet{background:var(--bg);opacity:0.65}
.docbox--quiet .docbox__h,.pquiet .docgroup{color:var(--ink)}
.docbox--quiet .btn-go,.docbox--quiet .btn-up,.pquiet .btn-go,.pquiet .btn-up{background:transparent;border-color:var(--line);color:var(--muted);font-weight:600}
.docbox--quiet .btn-go:hover,.docbox--quiet .btn-up:hover,.pquiet .btn-go:hover,.pquiet .btn-up:hover{border-color:var(--muted);color:var(--ink)}
/* 🔴 INSIDE A DIMMED BOX, TEXT STARTS AT FULL INK. The dim is the recessive signal; muting the colours
   AS WELL stacks two reductions and the composited result falls under the 4.5:1 floor — measured, not
   assumed (qa/paperwork-group-weight-check.php sweep). So the quiet group keeps ink-strength text and
   lets the opacity do the work. The assist control joins the same rule: at its stock #2b6cb0 it starts
   at 5.42:1 and cannot survive any dim at all. */
.docbox--quiet,.docbox--quiet .hint,.docbox--quiet .muted,.docbox--quiet .doc-hist,
.docbox--quiet .doc-hist summary,.docbox--quiet .doc-hist-meta,.docbox--quiet small,
.docbox--quiet label,.docbox--quiet .btn-assist,
.pquiet,.pquiet .hint,.pquiet .muted,.pquiet p,.pquiet small,.pquiet label{color:var(--ink)}
.docbox--quiet .btn-assist{border-color:var(--ink)}

/* ══ /photos — ONE LIBRARY (operator ruling 2026-09-08) ═══════════════════════════════════════════════
   🔴 ALMOST NOTHING IS NEW HERE, AND THAT IS THE POINT. The collapsed cards ARE /paperwork's cards: the
   view emits .prow / .prow__sum / .prow__top / .prow__name / .prow__chip / .prow__why and inherits every
   declaration above — the ruled chip colours included, so "red = your move, neutral = waiting on us"
   cannot come to mean two different things on two pages. What follows is the body (a photo album is not
   three document boxes), the strip, and the claim control.

   🔴 THE STRIP SHARES THE PARKED GROUP'S DECLARATION, IT DOES NOT RESTATE IT. `.pquiet` was added to the
   `.docbox--quiet` selectors above rather than given its own copy of `opacity:0.65` — that number was
   chosen by the contrast sweep in qa/paperwork-group-weight-check.php (0.60 left the assist control at
   4.36:1; 0.65 cleared it at 5.12:1), and a retyped copy of a measured value is how two surfaces drift
   apart until one of them is failing the floor. If the sweep ever picks a different number, both move. */
.pstrip{margin:var(--sp-3) 0;padding:var(--sp-2) var(--sp-3);border:1px solid var(--line);border-radius:12px}
.pstrip p{margin:.2em 0 0;font-size:13.5px;line-height:1.45}
.pstrip .docgroup{margin:0}

.pcards{margin:var(--sp-4) 0 0}
.pcards__h{font-size:var(--fs-1);font-weight:700;margin:0 0 var(--sp-2)}
.pcards__empty{margin:0 0 var(--sp-2)}
.pcards__empty p{margin:0 0 var(--sp-2);font-size:13.5px;line-height:1.45}

/* 🔴 ONE COLUMN, NOT A GRID, and it is the ruling's own consequence — the same reasoning /paperwork wrote
   down. These cards expand IN PLACE and several can be open at once; an expanding card inside a
   multi-column grid reflows its whole row, which is exactly the defect the /brands panel-host pattern
   exists to prevent. A single-column stack has no row geometry to disturb. A later "tidy this into a
   grid" is a regression, and qa/photos-page-check.php reds on it. */
.pcards .pcard{display:block;width:100%;margin:0 0 var(--sp-2)}
.pcard__badges{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end;flex:1 1 auto;min-width:0}
.pcard__body{padding:var(--sp-2) var(--sp-3) var(--sp-3);border-top:1px solid var(--line)}
.pcard__body .photogrid{margin-top:0}

/* THE VENDOR'S CLAIM (v217). A fieldset, because these options belong together and a legend says what the
   question is; quiet, because it is optional and answering it publishes nothing. */
.pclaim{border:1px solid var(--line);border-radius:10px;padding:var(--sp-2);margin:0 0 var(--sp-2)}
.pclaim__lg{font-size:13px;font-weight:700;padding:0 4px}
.pclaim__opt{display:inline-flex;gap:5px;align-items:center;margin:0 12px 4px 0;font-size:13px;min-height:var(--touch);line-height:1.3}
.pclaim__note{margin:.3em 0 0}

/* the link that replaced the uploader on a /brands card */
.bcard__photolink{margin-top:var(--sp-1);text-align:center}

/* ── THE PER-PHOTO TAG — CHIPS (operator ruling 2026-09-09) ───────────────────────────────────────
   🔴 CHIPS, NOT CHECKBOXES. Tap to select, tap to clear, and the tap is the save — no Save button, because
   one tap should be the whole interaction. Selected is FILLED in the tenant accent; unselected is an outlined
   pill. The inputs are still real inputs underneath (.ptag__chip input is visually hidden, never
   display:none — that would take the control out of the tab order and off the accessibility tree), so
   keyboard, screen readers and the admin's POSTed form all keep working on the same markup.
   🔴 CLOSED IS A CHIP, NOT A CARET, and an untagged photo shows an invitation rather than a label reading
   "Not tagged". Nothing about the closed state should look like a widget.
   🔴 THE PANEL FLOATS. .photogrid is repeat(auto-fill,minmax(150px,1fr)); a figure that grew when its panel
   opened would reflow the whole row — the defect the /brands panel-host ruling exists to prevent, and the
   reason /photos' own cards are a single column. */
.ptag{position:relative;margin-top:.35em}
.ptag__open{display:block;width:100%;text-align:left;background:none;border:0;padding:0;cursor:pointer;
  font:inherit;color:inherit;min-height:var(--touch)}
.ptag__add{display:inline-block;padding:3px 10px;border-radius:999px;font-size:12px;line-height:1.3;
  color:var(--muted);border:1px dashed var(--line);background:transparent}
.ptag__open:hover .ptag__add{border-color:var(--accent);color:var(--accent)}

/* the chip itself — one declaration, used closed (as a span) and open (as a label) */
.ptag__chip{display:inline-flex;align-items:center;gap:4px;margin:0 4px 4px 0;padding:4px 11px;
  border-radius:999px;font-size:12px;line-height:1.35;cursor:pointer;
  border:1px solid var(--line);background:var(--card);color:var(--ink);white-space:normal}
/* 🔴 THE FILL FOLLOWS THE INPUT, not only a JS class. `:has(input:checked)` means the chip is correct after a
   keyboard toggle, a browser restoring form state on back-navigation, or any path that never ran our handler —
   and it is what lets the SAME markup carry the same look in the admin's form, where nothing fetches at all.
   `.is-on` stays for the CLOSED summary chips, which are spans with no input inside them. */
.ptag__chip.is-on,.ptag__chip:has(input:checked){background:var(--chip-on);border-color:var(--chip-on);color:var(--chip-on-ink);font-weight:600}
/* 🔴 THE TICK IS A TOKEN AND THIS SURFACE DECLARES none — the pseudo-element is never generated, so the
   2026-09-09 ruling above still describes this page exactly: filled in the tenant accent, nothing else.
   The declaration is here and byte-identical to the admin's because that is the standing rule for this
   block; the only difference between the two surfaces is the value of --chip-tick in the palette. */
.ptag__chip.is-on::before,.ptag__chip:has(input:checked)::before{content:var(--chip-tick);flex:none;
  align-self:center;width:.32em;height:.58em;margin:-.12em .15em .03em 0;border:solid currentColor;
  border-width:0 2px 2px 0;transform:rotate(45deg)}
.ptag__chip:hover{border-color:var(--accent)}
/* 🔴 VISUALLY HIDDEN, NOT display:none — the input keeps its place in the tab order and on the
   accessibility tree, which is the whole reason the chips are labels and not divs. */
.ptag__chip input{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
.ptag__chip:focus-within{outline:2px solid var(--accent);outline-offset:2px}

.ptag__panel{position:absolute;z-index:5;left:0;top:100%;min-width:min(250px,84vw);
  background:var(--card);border:1px solid var(--line);border-radius:12px;padding:var(--sp-2);
  box-shadow:0 8px 24px rgba(0,0,0,.15)}
/* the photo again, above the chips: a vendor tagging a dish should be looking at it */
.ptag__shot{width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:8px;margin-bottom:var(--sp-2);display:block}
.ptag__chips{display:flex;flex-wrap:wrap}
/* the tick is quiet and it FADES — six tagged photos should not leave six confirmations on the page */
.ptag__state{margin:.3em 0 0;font-size:11.5px;line-height:1.3;min-height:1.3em;color:transparent;
  transition:color .25s ease}
.ptag__state.is-ok{color:var(--go)}
.ptag__state.is-bad{color:var(--stop)}
/* THE CHECKOUT GALLERY SWITCH (2026-09-16): one checkbox per photo, in the figcaption, every surface. It controls the checkout
   photo gallery only; the photo itself is never dimmed (operator 2026-09-16: "just the tick"); the state line fades like the tag tick. */
.gal-toggle{display:flex;align-items:center;gap:.4em;flex-wrap:wrap;margin:.35em 0 0;font-size:12px;line-height:1.3;cursor:pointer;color:var(--ink)}
.gal-toggle input{margin:0;width:15px;height:15px;flex:0 0 auto;accent-color:var(--go)}
.gal-toggle.is-off{color:var(--muted)}
.gal-toggle__state{flex-basis:100%;font-size:11.5px;min-height:1.3em;color:transparent}
.gal-toggle__state.is-ok{color:var(--go)}
.gal-toggle__state.is-bad{color:var(--stop)}

/* ════════════════════════════════════════════════════════════════════════════════════════════
   CALENDAR — the merged tab (operator ruling 2026-09-09). Schedule and Sync Calendars folded
   into /calendar as sub-tabs; the month grid inverted so that a DOT IS A BOOKING and an open
   day is blank. Appended as one fenced block rather than threaded through the sheet above:
   three seats were in this file the week it landed, and a block at the end merges cleanly.
   Tokens only — no colour is declared here that is not already named at :root, except the two
   truck-colour carriers, which take their value INLINE from Trucks::colorsFor() because a truck
   colour is data, not style.
   ══════════════════════════════════════════════════════════════════════════════════════════ */

/* ── sub-tabs ───────────────────────────────────────────────────────────────────────────────
   Text tabs on a hairline, matching the sidebar's text-only restraint. Not .btn: these are
   navigation between views of one page, and a row of buttons would read as four actions. */
.subtabs{display:flex;gap:var(--sp-3);border-bottom:1px solid var(--line);margin:var(--sp-3) 0}
.subtabs a{display:inline-block;padding:var(--sp-2) 0;margin-bottom:-1px;
  border-bottom:2px solid transparent;color:var(--muted);text-decoration:none;font-weight:600}
.subtabs a:hover{color:var(--ink)}
.subtabs a.on{color:var(--accent);border-bottom-color:var(--accent)}

/* ── truck filter chips ─────────────────────────────────────────────────────────────────────
   The swatch is the SAME .dot the grid draws, so the chip and the booking it filters to are
   visibly one thing. Links, not buttons — the grid is server-filtered. */
.chiprow{display:flex;gap:var(--sp-1);flex-wrap:wrap;margin-bottom:var(--sp-2)}
.tchip{display:inline-flex;align-items:center;gap:7px;min-height:var(--touch);
  padding:0 12px;border:1px solid var(--line);border-radius:999px;background:var(--card);
  color:var(--ink);text-decoration:none;font-weight:600;font-size:13px;white-space:nowrap}
.tchip:hover{background:var(--bg)}
.tchip.on{border-color:var(--accent);background:var(--accent-bg);color:var(--accent)}
.tchip .dot{width:10px;height:10px}

/* ── the two dots that are not a truck ──────────────────────────────────────────────────────
   🔴 A HOLLOW RING IS "No truck assigned" AND THAT IS A FINAL STATE (operator ruling
   2026-09-09), not a pending one: bookings sold before the marketplace will never carry a
   truck. It is a ring rather than a grey disc so it cannot be mistaken for an eighth truck
   colour. A dotted ring is the vendor's OWN gig — drawn, because it is something they have on,
   and outlined, because it is not a Food Truck Avenue booking. */
.dot--noassign{background:transparent !important;border:2px solid var(--muted)}
.dot--gig{background:transparent !important;border:2px dotted var(--muted)}
/* a TIMED block (a row from the vendor's calendar, or their own timed not-available): hollow, SOLID ring in the truck's colour —
   distinct from a booking (filled) and an own gig (dotted). Hour grain, 2026-09-11. */
.dot--busy{background:transparent !important;border:2px solid var(--muted)}
.ev__dot--busy{background:transparent !important;border:2px solid var(--muted);box-sizing:border-box}

/* ── SYNC TAB (rebuilt 2026-09-11, operator layout ruling): ONE TRUCK, ONE CARD, BOTH DIRECTIONS ──
   .synccard__grid puts "Your calendar to us" beside "Us to your calendar" on a wide screen and stacks them INSIDE
   the same card under 720px — a vendor standing next to the truck is the normal case. The source tabs above switch
   which provider's instructions show, never which trucks show. .syncsrc__pre is the precondition: said first,
   visually first (a left bar in the warn colour so it is read as "check this before you start", not as an error). */
.synctabs{margin-top:var(--sp-2);flex-wrap:wrap}
.syncsrc__pre{border-left:4px solid var(--warn-edge, var(--warn));padding-left:var(--sp-2);margin:var(--sp-2) 0}
.syncsrc__warn{border-left:4px solid var(--stop);padding-left:var(--sp-2);margin:var(--sp-2) 0}
.syncsrc__dirs{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-3);margin-top:var(--sp-2)}
.syncsrc__dir h3,.synccard__half h3{font-size:14px;margin:0 0 var(--sp-1)}
.syncsrc__dir ol{margin:0;padding-left:1.25em}
.syncsrc__dir li{margin:0 0 var(--sp-1)}
.synccard__grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-3);margin-top:var(--sp-2)}
.synccard__half{min-width:0;padding:var(--sp-2);border:1px solid var(--line);border-radius:var(--r)}
.synccard__half .linkbox{min-width:0;width:100%}
@media (max-width:720px){
  .syncsrc__dirs,.synccard__grid{grid-template-columns:1fr}
}

/* ── month grid ─────────────────────────────────────────────────────────────────────────────
   The cell is an <a> now (the day panel is a link, not a script). */
@media(min-width:700px){
  .mgrid{gap:4px}
  .mgrid .cell{display:flex;flex-direction:column;gap:3px;min-height:96px;padding:4px 5px;
    font-size:12px;line-height:1.3;overflow:hidden;text-decoration:none;color:inherit}
  .mgrid .cell:has(.cell__day):hover{border-color:var(--muted)}
  .cell--void{background:var(--bg);border-color:transparent !important}
  .cell--past{background:var(--bg)}
  /* 🔴 NO INVENTED HEX. This was #a9adb3 — a grey I picked because --muted (#6b7280) is the
     colour of a NORMAL day's number and a past one needs to sit under it. There is no lighter
     grey token, and adding one for a single dimmed numeral is how this stylesheet's third blue
     happened. Opacity is the same answer .cell--past .ev already uses one line down: one token,
     two weights. qa/paperwork-page-check.php E8 scans to the end of the file and caught it. */
  .cell--past .cell__n{color:var(--muted);opacity:.6}
  .cell--past .ev{opacity:.55}
  .cell--shut{background:var(--off)}
  .cell--today{box-shadow:inset 0 0 0 2px var(--accent)}
  .cell--sel{border-color:var(--accent) !important}
  .cell__n{font-weight:600;color:var(--muted);font-variant-numeric:tabular-nums;flex:none}
  .cell--today .cell__n{color:var(--accent)}
  .cell__dots{display:flex;gap:4px;flex-wrap:wrap}
  .cell__dots .dot{width:10px;height:10px}
  /* the count only appears when the dots have outrun the eye (>3, unfiltered) */
  .cell__n2{color:var(--muted);font-size:11px}
  /* THE reason, once per day, pinned to the bottom so cells with content stay aligned */
  .cell__why{margin-top:auto;color:var(--muted);font-size:11px;line-height:1.25;
    overflow:hidden;text-overflow:ellipsis}
  /* a booking with room to be itself: time + who, in the truck's colour */
  .ev{display:block;background:var(--card);border-radius:2px;
    padding:1px 4px;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .cell--past .ev,.cell--shut .ev{background:transparent}
  .ev__t{color:var(--muted);margin-right:5px;font-variant-numeric:tabular-nums}
  .ev--gig{background:transparent}

}
.callegend{display:flex;gap:var(--sp-4);flex-wrap:wrap;color:var(--muted);font-size:12px;
  margin:var(--sp-2) 0 0;padding-top:var(--sp-2);border-top:1px solid var(--line)}
.callegend span{display:inline-flex;align-items:center;gap:6px}
.callegend .dot{width:10px;height:10px}
@media(max-width:699.98px){.callegend{display:none}}

/* ── the day panel ──────────────────────────────────────────────────────────────────────────
   Where an own gig or a blocked day is removed: /schedule listed those rows separately, and
   with the grid drawing them that list became a second rendering of the same rows. */
.daypanel{margin-top:var(--sp-2)}
.dayrow{display:flex;align-items:flex-start;gap:var(--sp-2);padding:var(--sp-2) 0;
  border-bottom:1px solid var(--line)}
.dayrow:last-child{border-bottom:0}
.dayrow__bar{width:4px;align-self:stretch;min-height:2.2em;border-radius:2px;flex:none;
  background:var(--off)}
.dayrow__bar--gig{background:transparent;border-left:3px dotted var(--muted);width:4px}
.btn-sm{min-height:26px;padding:0 var(--sp-2);font-size:12px}

/* ── CALENDAR round 2 (operator, 2026-09-09) ─────────────────────────────────────────────────
   Day/week/month, the month+year jump, and the add-a-job panel moved off the bottom edge. */

/* THE VIEW BAR. One row: stepper · month+year jump · view switch, pushed apart by .grow. */
.calbar{display:flex;align-items:center;gap:var(--sp-1);flex-wrap:wrap;margin-bottom:var(--sp-2)}
.calbar__jump{display:flex;align-items:center;gap:var(--sp-1);margin:0}
.calbar__jump select{min-height:var(--touch);padding:0 var(--sp-1);border:1px solid var(--line);
  border-radius:var(--r);background:var(--card);color:var(--ink);font:inherit}
.calbar .grow{flex:1}
/* visually-hidden label — the selects need names for a screen reader, not on screen */
.vh{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}
.viewsw{display:inline-flex;border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
.viewsw a{display:inline-flex;align-items:center;min-height:var(--touch);padding:0 var(--sp-3);
  color:var(--muted);text-decoration:none;font-weight:600;font-size:13px;
  border-left:1px solid var(--line)}
.viewsw a:first-child{border-left:0}
.viewsw a:hover{background:var(--bg);color:var(--ink)}
.viewsw a.on{background:var(--accent-bg);color:var(--accent)}

/* WEEK VIEW — the month grid's own cell, seven of them, taller because there is room for the
   real thing. Never dots here: the ruling is that a dot is the fallback when there is not. */
@media(min-width:700px){
  .mgrid--week{grid-template-columns:repeat(7,1fr)}
  .mgrid--week .cell{min-height:320px}
}
@media(max-width:699.98px){
  /* on a phone a 7-column week is unreadable; it becomes a stack, one day per row */
  .mgrid--week{display:grid;grid-template-columns:1fr;gap:4px}
  .mgrid--week .cell{background:var(--card);border:1px solid var(--line);border-radius:var(--r);
    min-height:auto;padding:var(--sp-2);display:flex;flex-direction:column;gap:3px;
    font-size:12px;text-decoration:none;color:inherit}
  .mgrid--week .cell__n{font-weight:650;color:var(--muted)}
  .mgrid--week .ev{display:block;background:var(--bg);
    border-radius:2px;padding:1px 4px}
  .mgrid--week .ev__t{color:var(--muted);margin-right:5px;font-variant-numeric:tabular-nums}
  .mgrid--week .ev--gig{background:transparent}

  .mgrid--week .cell__why{color:var(--muted);font-size:11px}
  .mgrid--week .cell--past{background:var(--bg)}
  .mgrid--week .cell--shut{background:var(--off)}
  .mgrid--week .cell--today{box-shadow:inset 0 0 0 2px var(--accent)}
}

/* ── THE ADD-A-JOB PANEL ─────────────────────────────────────────────────────────────────────
   🔴 IT IS NO LONGER PINNED TO THE BOTTOM EDGE ON DESKTOP (operator, 2026-09-09: "it ties it to
   the bottom of the screen and it's hard to read — it should be left justified like Google").
   `.sheet` is a phone-first bottom sheet and that is still right on a phone; on a wide screen a
   full-width strip along the bottom edge is the worst shape a form can have — the label and its
   field end up a foot apart, which is this stylesheet's own recorded finding about the pricing
   form. From 700px up it becomes a fixed panel down the LEFT of the viewport: a column the eye
   reads in one pass, anchored where Google puts its event editor.
   Only .sheet--form takes this; the base .sheet keeps its phone behaviour for any future caller. */
@media(min-width:700px){
  /* 🔴 THE EDGE MOVED TO THE RIGHT IN ROUND 4 and this rule was EDITED, not overridden further
     down. Leaving the left-anchored inset here and cancelling it later would make which side the
     panel opens on depend on rule order in a 1,500-line stylesheet — two rules for one decision,
     and the loser still readable as if it were the answer. One rule, one edge. */
  .sheet--form{inset:0 0 0 auto;width:min(440px,92vw);max-height:100vh;border-radius:6px 0 0 6px;
    box-shadow:-8px 0 24px rgba(26,29,33,.16);
    padding:var(--sp-4) var(--sp-4) var(--sp-5);overflow:auto}
}
/* one screen, so the short fields pair up instead of running down a column of their own */
.fgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:0 var(--sp-2)}
.fgrid .f{grid-column:span 1}
.fgrid .f:nth-child(2){grid-column:1/-1}   /* the label line runs full width */
.sheet--form .f{display:block;margin-bottom:var(--sp-2);font-weight:600}
.sheet--form .f small{display:block;font-weight:400;color:var(--muted);margin-bottom:2px}
.sheet--form .f input,.sheet--form .f select{width:100%;min-height:var(--touch);
  padding:0 var(--sp-2);border:1px solid var(--line);border-radius:var(--r);
  background:var(--card);color:var(--ink);font:inherit;font-weight:400}
.sheet--form .fs{border:1px solid var(--line);border-radius:var(--r);
  padding:var(--sp-2) var(--sp-3) var(--sp-3);margin:0 0 var(--sp-3)}
.sheet--form .fs legend{font-weight:600;padding:0 var(--sp-1)}
.kind{display:flex;align-items:flex-start;gap:var(--sp-2);padding:var(--sp-1) 0;cursor:pointer}
.kind input{width:auto;min-height:auto;margin-top:3px;flex:none}
.kind em{font-style:normal;font-weight:400;color:var(--muted);display:block;font-size:.92em}

/* ── CALENDAR round 3 (operator walkthrough, 2026-09-09) ─────────────────────────────────────
   The day-header rule that was written in the mock and never shipped, the not-bookable banner,
   and the centred stepper. */

/* 🔴 THE DAY HEADER HAD NO RULE AT ALL, ON EITHER GRID. The markup shipped in round 2 and this
   never did, so `<div class="dow">` rendered as bare inline spans: "Mon 28Tue 29Wed 30Thu 1…"
   with no spacing, in month view as well as week. It was never escaping its wrapper — it was
   inside it, and the wrapper had no styling. It tracks the grid's own 7 columns and gap so a
   header can only ever sit over the column it names. */
.dow{display:grid;grid-template-columns:repeat(7,1fr);gap:4px;
  color:var(--muted);font-size:11px;font-weight:650;letter-spacing:.04em;text-transform:uppercase;
  margin:var(--sp-2) 0 4px}
.dow span{padding:0 5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media(max-width:699.98px){ .dow{display:none} }   /* the phone stacks; column heads mean nothing there */

/* 🔴 THE WEEK STRIP IS PHONE-ONLY NOW. On desktop the grid IS the navigator and the centred
   stepper moves it; a second row of the same seven days is what made a view change read as
   "a strip opened above the calendar". */
@media(min-width:700px){ .striprow{display:none} }

/* which trucks cannot take a booking — a header and one bullet per truck, never a run-on line */
.notbook{border-left:4px solid var(--warn-edge)}
.notbook__h{font-size:var(--fs-0);font-weight:650;margin:0 0 var(--sp-1)}
.notbook__list{margin:0 0 var(--sp-2);padding-left:1.15em}
.notbook__list li{margin-bottom:3px;line-height:1.4}

/* the bar: Today + jump on the left, the position between its own arrows in the middle, the
   view switch on the right. .calbar itself is already a flex row. */
.calbar__side{display:flex;align-items:center;gap:var(--sp-1);flex:1 1 0;min-width:0}
.calbar__side--end{flex:1 1 0;justify-content:flex-end}
.calbar__mid{display:flex;align-items:center;gap:var(--sp-2);flex:0 0 auto;justify-content:center}
.calbar__pos{font-size:var(--fs-1);font-weight:650;white-space:nowrap;text-align:center;
  min-width:11ch;font-variant-numeric:tabular-nums}
.calbar__arrow{min-width:var(--touch)}
@media(max-width:899.98px){
  /* stacked on a phone: the position and its arrows come first, everything else under them */
  .calbar{flex-direction:column;align-items:stretch}
  .calbar__mid{order:-1;justify-content:space-between}
  .calbar__side,.calbar__side--end{justify-content:flex-start;flex:0 0 auto}
}

/* ── CALENDAR / TRUCKS round 4 (operator walkthrough, 2026-09-09) ────────────────────────────── */

/* every outstanding item on a Taking-bookings row, not just the first */
.tbk__list{margin:2px 0 0;padding-left:1.15em;color:var(--muted);font-size:12px;line-height:1.45}
.tbk__list li{margin-bottom:2px}

/* 🔴 DAY · WEEK · MONTH IS A SEGMENTED CONTROL, NOT THREE LINKS (operator, round 4). It read as
   body text because it was styled like text: no fill, no pressed state, colour as the only
   signal. It is still three <a>s — navigation, and it must survive with no JS — but it now looks
   like the control it is: one bordered group, hairline dividers, and the ACTIVE segment filled
   with the accent so the selected view is unmistakable rather than merely tinted. */
.viewsw{border:1px solid var(--line);border-radius:var(--r);overflow:hidden;background:var(--card);
  flex:0 0 auto}
.viewsw a{border-left:1px solid var(--line);font-size:13px;padding:0 var(--sp-3);
  min-height:var(--touch);display:inline-flex;align-items:center;color:var(--ink);
  text-decoration:none;font-weight:600}
.viewsw a:first-child{border-left:0}
.viewsw a:hover{background:var(--bg)}
.viewsw a.on{background:var(--accent);color:var(--accent-ink)}
.viewsw a.on:hover{background:var(--accent)}

/* the day view wears the week view's frame: a header row, then the day's own panel */
.dayhead{display:grid;grid-template-columns:1fr;gap:4px;color:var(--muted);font-size:11px;
  font-weight:650;letter-spacing:.04em;text-transform:uppercase;margin:var(--sp-2) 0 4px}
.dayhead span{padding:0 5px}
@media(max-width:699.98px){ .dayhead{display:none} }
.daypanel{border:1px solid var(--line)}

/* the add-a-job form's own validation message — named field, above the control that failed */
.fmsg{border-left:4px solid var(--stop);background:var(--stop-bg);color:var(--ink);
  padding:var(--sp-2) var(--sp-3);border-radius:var(--r);margin:0 0 var(--sp-3);font-size:13px}
.fmsg[hidden]{display:none}
.f--bad input,.f--bad select{border-color:var(--stop)}

/* ── round 5 (operator walkthrough, 2026-09-09) ──────────────────────────────────────────────── */

/* the row a refusal belongs to marks itself — with five trucks, a message alone cannot say which */
.rowcard--bad{border-left-color:var(--stop) !important;background:var(--stop-bg)}

/* 🔴 THE STEPPER SITS IN THE TRUE CENTRE OF THE BAR (operator, round 5). It was three flex items
   with `flex:1 1 0` on the outer two, which equalises their BASIS and not their rendered width:
   the left side carries Today plus two selects plus Go, the right just the view switch, so the
   middle was pushed right by the difference. A three-column grid with equal 1fr outer tracks
   centres the middle track regardless of what either side holds — the geometry does the centring
   rather than the content happening to balance. */
@media(min-width:900px){
  .calbar{display:grid;grid-template-columns:1fr auto 1fr;align-items:center}
  .calbar__side{justify-self:start}
  .calbar__side--end{justify-self:end}
  .calbar__mid{justify-self:center}
}

/* ── round 6 (operator walkthrough, 2026-09-09) ──────────────────────────────────────────────── */

/* 🔴 THE MONTH CONTROL COULD NOT RENDER ITS OWN VALUE ("Septe…"). Two causes, both needed fixing:
   the selects had no width floor, and `.calbar__side` carried `min-width:0`, which lets a grid
   track shrink below its content. The floor is expressed in `ch` off the LONGEST option rather than
   as a picked pixel value — September and December are the widest at nine characters, plus the
   dropdown arrow — so it stays correct if the labels are ever translated or abbreviated.
   `flex:none` stops the selects being the thing that gives way when the row is tight. */
.calbar__side{min-width:max-content}
.calbar__jump select{flex:none}
.calbar__jump select[name="m"]{min-width:12ch}
.calbar__jump select[name="y"]{min-width:8ch}
/* below the grid breakpoint the bar stacks, so the row may wrap rather than squeeze the controls */
@media(max-width:899.98px){ .calbar__side{min-width:0;flex-wrap:wrap} }

/* ── round 8 (operator walkthrough, 2026-09-10) ─────────────────────────────────────────────── */

/* 🔴 THE MONTH AND YEAR CONTROLS PUSHED THE PAGE OFF A PHONE. `input,select` carries `width:100%`
   — the right default for a stacked form, and wrong for two controls sitting in a toolbar row: at
   390px each select took the FORM's whole width, so the pair plus Go measured 604px against a
   390px viewport and the document scrolled sideways. Round 6 gave them a `ch` floor and never
   lifted the 100%, and the desktop grid track was wide enough to hide it. `width:auto` lets the
   floor be the floor; the form wraps rather than overflowing when even that will not fit.
   Found by qa/portal-rendered-width-check.php R5(390), which is the leg for exactly this. */
.calbar__jump select{width:auto}
@media(max-width:560px){ .calbar__jump{flex-wrap:wrap} }

/* ── /calendar never scrolls the PAGE sideways, phone to laptop (2026-09-16, portal-leftovers seat) ──────────────
   qa/portal-rendered-width-check.php R5(390) was red on /calendar; widened to week/day views and 360px it found
   four causes, each a track or a line that took its CONTENT's width instead of the space it had. Every rule below
   changes geometry only where the page overflowed — at 1920/1280 and at 1024 in month + week view the layout is
   byte-identical (element-rect hash, session doc §2). CSS only: no markup, no script, no navigation.
   🔴 `1fr` IS `minmax(auto,1fr)` — a track never shrinks below its content.
   (1) PHONE WEEK STRIP: each day carries a row of dots, one per booking, that did not wrap; a nine-booking day made
       its column ~70px and the strip 408px on a 375px screen. Tracks take `minmax(0,1fr)`, the dots wrap.
   (2) PHONE WEEK STACK: a one-column grid with `1fr`; `.ev__n` is nowrap so it can ellipsis, and the track grew to
       the longest name (657px) so the ellipsis never engaged. `minmax(0,1fr)` lets it.
   (3) PHONE DAY BAR: "Wednesday 16 September 2026" is nowrap between two 48px arrows — 402px. Below 560px it wraps.
   (4) 900-1149px (the sidebar appears at 900): the centred three-track bar needs ~780px in month/week and ~900px in
       day view and the column had ~670. From 1000px the position label may wrap (the side controls never squeeze);
       below 1000px there is not room for both sides and the stepper at all, so the bar stacks exactly as it does under
       900. ⚠️ Two cuts measured and dropped: shrinking the middle TRACK to 0 let ‹ sit on Go and › on Day (an overflow
       probe cannot see an overlap — R6 now does), and it was not load-bearing once the label could wrap (plant P4 was
       green with it removed). */
@media(max-width:699.98px){
  .strip{grid-template-columns:repeat(7,minmax(0,1fr))}
  .strip a{min-width:0}
  .strip .dots-row{flex-wrap:wrap;justify-content:center;max-width:100%}
  .mgrid--week{grid-template-columns:minmax(0,1fr)}
  .mgrid--week .cell{min-width:0}
}
@media(max-width:560px){
  .calbar__mid{min-width:0}
  .calbar__pos{white-space:normal;min-width:0;flex:1 1 auto;overflow-wrap:anywhere}
}
@media(min-width:900px){
  .calbar__pos{white-space:normal;min-width:0}
}
@media(min-width:900px) and (max-width:999.98px){
  .calbar{display:flex;flex-direction:column;align-items:stretch}
  .calbar__mid{order:-1;justify-content:space-between}
  .calbar__side,.calbar__side--end{justify-content:flex-start;flex:0 0 auto}
}

/* ── WHICH ITEMS A PACKAGE INCLUDES — the choice-set line and panel (operator rulings 1-4,
   2026-09-10; qa/PROPOSAL-2026-09-10-package-choice-sets.md) ─────────────────────────────────

   🔴 NO NEW COLOUR AND NO NEW BANNER. Every rule below paints with tokens this file already
   declares. In particular there is deliberately NO amber box: the 2026-09-08 ruling reserves amber
   for a state that wants a RESPONSE, and a premium note states a consequence of a value the vendor
   just typed. --warn appears here only as an INK on a word, which is the role that token keeps. */

/* WHAT THE CUSTOMER MAY ORDER — a labelled radio pair under a priced package row (2026-09-10, fourth pass); spans the
   .pkg grid. 🔴 The radios are STANDARD and INLINE with their label: the global `input{min-height:var(--touch)}` +
   `label{display:block}` rules turned them into 44px blocks sitting above their words ("two floating dots"). Same reset
   the estate's other radio rows use (.notoffer, .pkg-min__opt). */
.pickctl{grid-column:1/-1;margin-top:var(--sp-1);padding:8px var(--sp-2);border-radius:var(--r);background:var(--bg);border:1px solid var(--line);
  font-size:12.5px;line-height:1.5}
.pickctl__q{font-weight:650;color:var(--ink);margin:0 0 4px}
.pickctl .pickopt{display:flex;align-items:baseline;gap:8px;margin:2px 0;font-weight:400;font-size:12.5px;cursor:pointer}
.pickctl .pickopt input[type=radio]{appearance:auto;width:14px;height:14px;min-height:0;margin:0;padding:0;flex:none;position:relative;top:2px;accent-color:var(--accent);cursor:pointer}
.pickctl .pickopt>span{min-width:0}
.pickctl select{width:auto;min-width:3.4em;min-height:0;height:auto;font-size:12.5px;padding:1px 4px;margin:0 1px;vertical-align:baseline}
.pickctl__note{margin:4px 0 0 22px}
.pickctl__ro{color:var(--ink)}
.pkgname .pkg-range{display:block;color:var(--warn);font-weight:650}
/* our pick limits on a house-brand row (2026-09-10): read-only, clearly ours, never a control */
.pkgname .pkg-limits{display:block;font-size:12px;color:var(--ink)}
.pkgname .pkg-limits__who{display:inline-block;font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);border:1px solid var(--line);border-radius:4px;padding:0 4px;margin-right:4px;vertical-align:1px}

/* the item's premium box + revealed price (2026-09-10, fourth pass): a NORMAL small checkbox inline with a two-word
   label, the price directly underneath, indented under the box; the explainer lives in the step header, once */
.bw-premium{margin:2px 0 0}
.bw-premium__on{display:inline-flex;align-items:center;gap:6px;margin:0;font-weight:400;font-size:12.5px;cursor:pointer}
.bw-premium__on input[type=checkbox]{appearance:auto;width:14px;height:14px;min-height:0;margin:0;padding:0;flex:none;accent-color:var(--accent);cursor:pointer}
.bw-premium__price{margin:3px 0 0 20px}
.bw-premium__price label{display:inline-flex;align-items:center;gap:4px;margin:0;font-weight:400;font-size:12.5px}
.bw-premium__price input[type=number]{width:74px;min-height:0;height:26px;padding:0 5px;text-align:right;font-variant-numeric:tabular-nums}
.bw-premium__price .cur,.bw-premium__price .per{color:var(--muted);white-space:nowrap}
/* tighter cards (2026-09-10): a small thumbnail beside the Add photo button, no dashed block; six on a screen, not three */
.bw-item{padding:var(--sp-2);gap:2px}
.bw-item textarea{min-height:2.3em}
.bw-item .bw-photo{display:flex;flex-wrap:nowrap;align-items:center;gap:var(--sp-2);margin-top:4px}
.bw-item .bw-photo .bw-item-tile{width:40px;height:40px;flex:0 0 40px}
.bw-item .bw-photo .bw-photo-text{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bw-item .bw-mini-upload{flex:0 0 auto;margin:0}
.bw-item .dropzone{border:0;padding:0;background:none;display:flex;flex-direction:row;align-items:center;gap:var(--sp-1);text-align:left}
.bw-item .dz-btns{margin:0}
.bw-item .dz-count{min-height:0;margin:0;font-size:12px}
.bw-item .dz-count:empty{display:none}
.bw-item .dz-previews{margin:0}
.bw-item .dz-previews img{width:40px;height:40px}

/* ── the platform-job view (operator ruling 2026-09-10, round 6 item 5) ──────────────────────── */

/* A LABEL/VALUE LIST, NOT A TABLE. Six facts about one job is a description list, and a table
   would need a header row saying nothing. Grid so the values line up down the page; it collapses
   to stacked rows on a phone, where a 7em label column would leave the value four words wide. */
.jrow{display:grid;grid-template-columns:7.5em 1fr;gap:var(--sp-2);
  padding:var(--sp-1) 0;border-top:1px solid var(--line)}
.jrow:first-of-type{border-top:0}
.jk{color:var(--muted);font-size:12px}
.jv{min-width:0;overflow-wrap:anywhere}
@media(max-width:560px){
  .jrow{grid-template-columns:1fr;gap:2px}
}

/* ── THE UNANSWERED EXTRA-HOUR STATE (operator ruling 2026-09-10) ─────────────────────────────
   A rate row has three states, and only two of them used to look like anything: a rate typed, and
   "Not offered" ticked. The third — nothing typed, nothing ticked — rendered identically to a row
   the vendor simply had not reached yet, while silently blocking every booking on that truck.
   🔴 AMBER, NOT RED, and 4px like the /paperwork edges: this is unfinished, not wrong, and the
   2026-09-08 finding on that page was that a thin bar reads as almost nothing. --warn-edge is the
   BAR token chosen there by measurement (ΔE2000 45.0 from --stop, ΔL* 33.1); it is never an ink.
   The sentence carries the meaning — the colour only makes the row findable in a list. */
.pf.is-undecided { border-left: 4px solid var(--warn-edge, #f2b705); padding-left: calc(var(--sp-2) - 4px); }
.pf.is-undecided.is-off { border-left-color: transparent; }   /* ticked wins: it IS answered */
.undecided-note { margin: .35em 0 0; font-size: .9em; color: var(--ink-2, #4a4a4a); }
.undecided-note b { font-weight: 600; }

/* ── the truck colour picker (operator ruling 2026-09-10) ────────────────────────────────────── */

.cpick{display:flex;flex-wrap:wrap;align-items:center;gap:4px;margin:0}
/* 🔴 A SWATCH IS A BUTTON, so it carries a real focus ring and a real hit target. 22px with a 4px
   gap keeps twelve inside a phone's width on two rows and stays above the 24px CSS-pixel floor for
   a pointer target once the gap is counted. */
.cpick__sw{width:22px;height:22px;min-height:0;padding:0;border:1px solid rgba(0,0,0,.25);
  border-radius:var(--r);cursor:pointer;position:relative;flex:none}
.cpick__sw:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
/* the CURRENT colour wears a ring, not a tick: a tick inside a dark swatch is unreadable on half
   the palette, and the ring reads on every one of them */
.cpick__sw.is-mine{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--ink)}
/* TAKEN is a diagonal bar — a second visual channel, so it is not colour-on-colour. It marks, it
   does not disable: the vendor may still pick it (operator ruling). */
.cpick__sw.is-taken::after{content:"";position:absolute;inset:0;border-radius:inherit;
  background:linear-gradient(135deg,transparent 44%,rgba(255,255,255,.95) 44%,rgba(255,255,255,.95) 56%,transparent 56%)}
.cpick__auto{margin-left:2px}
/* the calendar's picker is folded away by default — twelve swatches per truck is a lot of colour
   to put above a grid whose whole job is showing bookings. <details> so it costs no JS. */
.cpickwrap{margin:0 0 var(--sp-2)}
.cpickwrap > summary{cursor:pointer;color:var(--muted);font-size:12px;padding:2px 0}
.cpickrow{gap:var(--sp-2);min-height:0;padding:2px 0}
/* on a card the picker is a block under the description, not a row */
.bcard .cpick{margin:0 0 var(--sp-1)}

/* ── notes for the team (operator ruling 2026-09-10) ─────────────────────────────────────────── */

/* the base rule styles input+select and stops there, so a textarea inherited the browser's default
   and sat next to fields it did not match. Same tokens, same border, and it grows vertically only —
   a horizontal drag inside a fixed-width sheet just breaks the layout. */
.sheet--form textarea{width:100%;font:inherit;font-size:13px;padding:6px var(--sp-2);
  border:1px solid var(--line);border-radius:var(--r);background:var(--card);color:var(--ink);
  resize:vertical;min-height:4.5em}
/* on the day panel and in the popup the notes are READ, so they keep the vendor's own line breaks
   without letting a long unbroken string push the row sideways */
.jnotes{white-space:pre-wrap;overflow-wrap:anywhere;margin:var(--sp-1) 0 0;
  padding-left:var(--sp-2);border-left:2px solid var(--line);color:var(--ink)}

/* ── the picker lives with the pill (operator ruling 2026-09-10) ─────────────────────────────── */

/* each truck is a small stack: its pill, and the colours directly beneath. align-items:start on the
   row keeps the pills on one baseline whether or not the swatches are showing. */
.chiprow{align-items:flex-start}
.tchipwrap{display:flex;flex-direction:column;gap:4px;align-items:flex-start}
.tchipwrap .cpick{max-width:16em}
/* the open/close control reads as an action rather than a filter, so it never looks like a truck */
.tchip--act{border-style:dashed;color:var(--muted)}

/* ── the event popup (operator ruling 2026-09-10) ────────────────────────────────────────────── */

/* 🔴 A DIALOG OVER THE GRID, NOT A PANEL ABOVE IT. The panel-above-the-grid was retired in round 4
   for good reason — it pushed the calendar down and read as part of the page. This floats, the grid
   stays where it was, and the scrim makes clicking away the obvious close. */
.scrim--open{position:fixed;inset:0;background:rgba(0,0,0,.28);z-index:9;display:block}
.evpop{position:fixed;z-index:10;left:50%;top:12vh;transform:translateX(-50%);
  width:min(30rem,calc(100vw - var(--sp-3) * 2));max-height:76vh;overflow:auto;
  box-shadow:0 10px 30px rgba(0,0,0,.22)}
@media(max-width:560px){
  /* on a phone it sits at the bottom, where a thumb is — the same place the add-job sheet opens */
  .evpop{top:auto;bottom:0;left:0;transform:none;width:100%;max-height:82vh;border-radius:var(--r) var(--r) 0 0}
}
/* an event in the grid is a control now, so it has to look like one on hover and focus */
a.ev:hover,a.ev:focus-visible{filter:brightness(.96);text-decoration:none}
a.ev:focus-visible,a.dot:focus-visible{outline:2px solid var(--ink);outline-offset:1px}
a.dot{display:inline-block}
/* the day link covers the cell beneath its contents, so an empty area still opens the day and an
   event above it opens its own popup. z-index only — no pointer-events games, which break focus. */
.mgrid .cell{position:relative}
.cell__day{position:absolute;inset:0;z-index:0;border-radius:inherit}
.cell__day:focus-visible{outline:2px solid var(--ink);outline-offset:-2px}
.mgrid .cell > .cell__n,.mgrid .cell > .cell__n2,.mgrid .cell > .ev,
.mgrid .cell > .cell__dots,.mgrid .cell > .cell__why{position:relative;z-index:1}

/* ── a job on the grid reads as a job (operator ruling 2026-09-10) ───────────────────────────── */

/* 🔴 THE COMPLAINT: "from across the room I cannot tell which truck it belongs to". The truck's
   colour was a 3px left border and the text was link blue with an underline, so the eye read
   "link" first, "colour" not at all, and every job on the grid looked like every other one.
   Dot, then time, then the name in INK — the dot carries identity, the text carries meaning, and
   neither is competing with the other. The left border is gone: it was doing the dot's job badly. */
.mgrid .ev,.mgrid--week .ev{display:flex;align-items:baseline;gap:5px;border-left:0;
  color:var(--ink);text-decoration:none;background:var(--card);border-radius:2px;
  padding:1px 4px;font-size:11px;line-height:1.35;overflow:hidden}
.mgrid a.ev,.mgrid--week a.ev{color:var(--ink);text-decoration:none}
.mgrid a.ev:hover,.mgrid--week a.ev:hover{background:var(--bg);text-decoration:none}
/* the dot sits ON the text baseline rather than in the middle of the row — a circle centred on a
   line of 11px text reads as floating above it */
.ev__dot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--muted);
  position:relative;top:1px}
.ev__t{color:var(--muted);margin:0;font-variant-numeric:tabular-nums;flex:none}
/* the NAME is the thing that must survive a narrow cell, so it is the only part allowed to clip */
.ev__n{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* 🔴 NO TRUCK = THE HOLLOW RING, the same mark the legend above the grid already uses for it. The
   old treatment was a doubled left border, which matched nothing else on the page. */
.ev--noassign .ev__dot{background:transparent;border:2px solid var(--muted);width:8px;height:8px}
/* an own gig keeps its distinct mark — a ring in the truck's colour rather than a filled disc — so
   "mine" and "booked through us" stay tellable apart at the same glance the colour is read at */
.ev--gig{background:transparent}
.ev--gig .ev__dot{box-shadow:inset 0 0 0 2px var(--card)}
/* the day rows use the same mark as the grid; they sit on larger text, so the dot is a touch bigger
   and pinned to the first line rather than centred on a row that may run to three */
.dayrow > .ev__dot{width:10px;height:10px;margin-top:.35em;top:0;align-self:flex-start}
.ev__dot--noassign{background:transparent !important;border:2px solid var(--muted)}
.ev__dot--gig{box-shadow:inset 0 0 0 2px var(--card)}

/* ── the job log (operator ruling 2026-09-10) ────────────────────────────────────────────────── */

.joblog{margin-top:var(--sp-3);border-top:1px solid var(--line);padding-top:var(--sp-2)}
.joblog__h{font-size:12px;font-weight:650;margin:0 0 var(--sp-1);color:var(--muted);
  text-transform:uppercase;letter-spacing:.04em}
.joblog__add{display:flex;flex-direction:column;gap:var(--sp-1);align-items:flex-start}
.joblog__add textarea{width:100%;font:inherit;font-size:13px;padding:6px var(--sp-2);
  border:1px solid var(--line);border-radius:var(--r);background:var(--card);color:var(--ink);
  resize:vertical;min-height:3em}
.joblog__list{list-style:none;margin:var(--sp-2) 0 0;padding:0;display:flex;flex-direction:column;gap:var(--sp-2)}
/* each entry is a record, so it reads as one: the stamp small and quiet, the words full size */
.joblog__e{padding-left:var(--sp-2);border-left:2px solid var(--line)}
.joblog__m{font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums}
.joblog__t{white-space:pre-wrap;overflow-wrap:anywhere}
/* parking instructions are a paragraph we were sent, so they keep their own line breaks */
.jparking{white-space:pre-wrap;display:block}

/* ── photos on a job (operator ruling 2026-09-10) ────────────────────────────────────────────── */
.jphotos{display:grid;grid-template-columns:repeat(auto-fill,minmax(88px,1fr));gap:var(--sp-1);margin-top:var(--sp-2)}
.jphoto{margin:0}
.jphoto img{width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:var(--r);display:block}
/* dimmed until approved — the SAME 0.6 as .photogrid, .bw-item-tile and .bw-logo, so a vendor reads
   one signal on every surface. ⚠ declared per-grid because the other three are; a single
   unscoped img.is-dim would be a fourth authority over the same look rather than a consolidation. */
.jphotos img.is-dim{opacity:.6}
/* a rendition the media service has not written yet occupies the SAME box as a photo, so the grid
   does not reflow when it arrives, and says what it is rather than collapsing to a bare caption */
.jphoto__none{width:100%;aspect-ratio:4/3;border-radius:var(--r);display:flex;align-items:center;
  justify-content:center;text-align:center;font-size:11px;color:var(--muted);
  background:var(--bg-2,#f3f3f1);border:1px dashed var(--line)}
.jphoto figcaption{font-size:11px;margin-top:2px}
/* the three .jphoto--ok/--wait/--no caption colours are DELETED, not left inert: this lane now
   renders the shared .chip, and a rule describing a retired treatment reads as evidence it is
   still in use (the same reason the right-column log rules went on 2026-09-10). */
/* three states, three words — a photo waiting on us must not look like one we turned down */

/* ── the booking popup, two columns (operator ruling 2026-09-10) ─────────────────────────────── */

/* 🔴 THE PROBLEM WAS HEIGHT, NOT WIDTH. Photos sat below the fold and a photo control nobody
   scrolls to is a photo control nobody uses. Details left, photos right, both in the first screen.
   The popup widens to hold them; it was sized for one column of facts. */
.evpop:has(.jpop2){width:min(58rem,calc(100vw - var(--sp-3) * 2))}
.jpop2{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:var(--sp-3);align-items:start}
.jpop2__l,.jpop2__r{min-width:0}
/* the photo column starts at the top of the dialog, not after the details it sits beside */
/* the log is never in the right column: both popups keep it in .jpop2__l under the controls
   (2026-09-10 round 17). The two rules that de-chromed a first-in-right log are DELETED, not
   left inert — a rule describing a retired layout is read as evidence the layout still exists. */
@media(max-width:760px){
  /* 🔴 STACKED ON A PHONE. Two columns at this width makes each one too narrow to read and the
     photo tiles too small to judge — the split exists to save vertical space, and there is none to
     save when the columns are 150px wide. */
  .jpop2{grid-template-columns:1fr;gap:var(--sp-2)}
}

/* ── the calendar swaps in place (operator ruling 2026-09-10) ────────────────────────────────── */

/* 🔴 NO FLASH. The region dims slightly while a fetch is in flight and nothing moves — no spinner
   that reserves height, no collapse to zero, so the grid does not jump under the cursor. */
#calswap.is-swapping{opacity:.6;transition:opacity .12s linear}
#calswap{min-height:1px}
@media(prefers-reduced-motion:reduce){ #calswap.is-swapping{transition:none} }
/* a failed swap replaces the grid rather than leaving a month on screen that is not the one asked
   for — the message says so and offers the ordinary navigation */
.calswap-err{border-color:var(--stop)}

/* ── the own-gig popup (operator ruling 2026-09-10) ──────────────────────────────────────────── */

/* a gig gets two columns like a booking; a BLOCKED DAY gets one, because it has no photos and
   nothing else to put beside the date and the reason */
.evpop--2{width:min(58rem,calc(100vw - var(--sp-3) * 2))}
.jpop2--one{grid-template-columns:1fr}
/* the edit form inside the popup is a block, not a second modal — a modal inside a modal is two
   scrims and a vendor cannot tell which Close belongs to which */
.sheet--inline{margin-top:var(--sp-2);border-top:1px solid var(--line);padding-top:var(--sp-2)}
.sheet--inline .fgrid{gap:var(--sp-1)}
.sheet--inline textarea,.sheet--inline input,.sheet--inline select{width:100%}

/* ── THE WINDOW BAR (2026-09-10, operator ruling: a staff read-only window into a vendor's portal) ──
   Above the shell on every page of a window. Amber = this sheet's ATTENTION family (the paperwork bar),
   deliberately not a vendor state colour, so it can never be read as vendor chrome. The edge is the
   4px rule from the paperwork finding: a thin bar reads as nothing. */
.window-bar{display:flex;align-items:center;gap:var(--sp-3);flex-wrap:wrap;padding:var(--sp-1) var(--sp-3);
  background:var(--warn-bg-strong);color:var(--warn-ink);border-bottom:4px solid var(--warn-edge);font-size:13px}
.window-bar__who{font-weight:700}
.window-bar__ro{font-weight:600;letter-spacing:.03em;text-transform:uppercase;font-size:11px;
  border:1px solid var(--warn-ink);border-radius:var(--r);padding:1px 6px}
.window-bar__left{font-variant-numeric:tabular-nums}
.window-bar__close{margin-left:auto}
.window-bar__close .btn{background:var(--warn-ink);border-color:var(--warn-ink);color:#fff}
.window-closed{max-width:32rem;margin:var(--sp-4) auto;padding:var(--sp-3);background:var(--card);
  border:1px solid var(--line);border-radius:var(--r)}

/* 🔴 THE CALENDAR PICKER LIST (2026-09-12). Measured cause, not a guess: the global `input,select{…width:100%}` above
   applies to RADIOS too, so a radio inside a flex `.row` takes the whole line and collapses its label to ZERO width —
   headless measurement at 1920/1280/390 gave radio 124px, text 0px, and a 31-character calendar name wrapping over
   SIX lines, hundreds of pixels from its button. `.decl-opt input[type=checkbox]` and `.w9-box input[type=radio]`
   already carry this workaround; this is the THIRD instance, which is why the root cause is reported rather than
   patched a third time in silence. The list uses its own classes and touches neither .row nor .grow. */
.calpick{margin:var(--sp-2) 0 0}
.calpick__opt{display:flex;align-items:flex-start;gap:var(--sp-2);margin:0;padding:var(--sp-2) 0;
  font-weight:400;font-size:13px;border-bottom:1px solid var(--line);cursor:pointer}
.calpick__opt:last-of-type{border-bottom:0}
.calpick__opt>input[type=radio]{width:auto;min-height:0;flex:none;margin:3px 0 0;padding:0}
.calpick__txt{flex:1 1 auto;min-width:0}
.calpick__name{display:block;font-weight:600}
.calpick__name em{font-weight:400;font-style:normal;color:var(--muted)}
.calpick__meta{display:block;color:var(--muted);font-size:12px;overflow-wrap:anywhere}

/* The picker's per-calendar count (2026-09-13) — the line that makes four identically-named calendars a real
   choice. An empty one is called out rather than shown as just another number: picking it is the mistake. */
.calpick__count{display:block;font-size:12px;color:var(--ink)}
.calpick__count--empty{color:var(--bad)}

/* +N more — the month cell's overflow link (2026-09-13). Rows are the default now; this is what a day
   with more events than fit says, and it goes to the day view rather than being a dead count. */
.mgrid .ev--more,.mgrid--week .ev--more{color:var(--muted);font-size:11px;background:transparent;padding-left:2px}
.mgrid a.ev--more:hover,.mgrid a.ev--more:focus-visible{color:var(--ink);text-decoration:underline}


/* 🔴 EVERY CLASS HERE IS PREFIXED `mny-`, AND THAT IS NOT COSMETIC (2026-09-14). The first cut
   used `.jrow` / `.jcell` — and `.jrow` IS AN EXISTING COMPONENT (portal.css:1738): a label/value
   grid rendered by _calendar_job_popup.php, _calendar_event_popup.php, job.php,
   _change_request_card.php and change_confirm.php. This block sits LATER in the sheet at the same
   specificity, so for as long as it shipped it silently re-laid-out FIVE other screens into an
   eight-column money grid. Nothing reds on that: no harness renders those five, and the money page
   itself looked fine.
   ⚠️ Found by MEASURING the page in a browser, not by reading the sheet — the operator reported
   boxes stacking, the source said `display:flex`, and both were true. A generic class name in a
   shared single-stylesheet portal is a collision waiting for a second component. */
/* ⚠️ A COMMENT MUST NOT CONTAIN ITS OWN TERMINATOR, AND A TOKEN LIST IS WHERE THAT HAPPENS.
   The line below once separated token names with slashes and ended a group with a star token —
   which put a star-slash pair inside the comment and CLOSED IT THERE. The prose after it became
   CSS, and the parser's error recovery discarded the next brace block it found, which was
   `.mny-cards{display:flex…}`. One rule silently gone; the source read correctly and the boxes
   stacked. Same family as an `accept` attribute opening a comment in the button gate (2026-09-12).
   🔴 Separate token names with COMMAS, never slashes, and never end one with a star.
   ⚠️ And writing this warning down is itself the hazard: the first draft of this very comment
   quoted the terminator literally and closed itself at the quote. It is described here, not shown. */
/* ══ MONEY: HEADER CARDS, THE JOB LIST, DRILL-DOWN SLIPS ════════════════════════════════════
   Composed from the existing vocabulary (--card, --line, --ink, --muted, --accent, --warn, --sp-N, --r)
   — no new tokens, no new colour names.
   🔴 RED IS DESTRUCTIVE ONLY (2026-09-11/12): the "owed to us" box states a balance, so --warn. */

/* ── THE HEADER CARDS: ONE HORIZONTAL ROW ────────────────────────────────────────────────────
   🔴 FLEX WITH `flex:1 1 0`, NOT `grid auto-fit`. auto-fit decides the column count from a track
   minimum, so the row silently becomes a stack the moment a figure or a container makes the
   minimum unsatisfiable — which is what the operator saw: five boxes reading as five more table
   rows before the table began. `flex:1 1 0` + `min-width:0` gives five EQUAL boxes that stay side
   by side and only wrap when the viewport genuinely cannot hold them. Equal width comes from the
   `0` basis: with `flex-basis:auto` the longest figure would take the widest box. */
.mny-cards{display:flex;flex-wrap:wrap;gap:var(--sp-2);margin:0 0 var(--sp-2)}
.mny-card{flex:1 1 0;min-width:8.5em;background:var(--card);border:1px solid var(--line);
  border-radius:var(--r);padding:var(--sp-2);display:flex;flex-direction:column;gap:1px}
.mny-card__n{font-size:22px;font-weight:650;line-height:1.1;color:var(--ink);
  font-variant-numeric:tabular-nums;letter-spacing:-.01em;overflow-wrap:anywhere}
.mny-card__k{font-size:12px;font-weight:600;color:var(--ink)}
/* "67 of 157" — the denominator is context, not a second figure, so it never competes with the
   count it qualifies. */
.mny-card__of{font-size:13px;font-weight:500;color:var(--muted);letter-spacing:0}
.mny-card__s{font-size:11px;color:var(--muted)}
.mny-card.is-lead{border-color:var(--accent);background:var(--accent-bg)}
.mny-card.is-lead .mny-card__n{color:var(--accent)}
.mny-card.is-owed{border-color:var(--warn);background:var(--warn-bg)}
/* Only when the row genuinely cannot hold five: two up, then one. */
@media (max-width:860px){ .mny-card{flex:1 1 30%} .mny-card__n{font-size:19px} }
@media (max-width:520px){ .mny-card{flex:1 1 45%} .mny-card__n{font-size:18px} }

/* ── THE JOB LIST ────────────────────────────────────────────────────────────────────────────
   ONE grid template, shared by the header, every row and the totals row — the columns stay in
   line because they are the same template, never because three copies were measured to agree. */
.mny-wrap{padding:0;overflow-x:auto}
.mny-table{min-width:820px;font-size:var(--fs-0)}
/* 🔴 ONE TEMPLATE, DECLARED ONCE ON THE CONTAINER AND INHERITED (2026-09-14). It was declared on
   `.mny-row` itself, so the header and every body row were SEPARATE grids — measured in a browser:
   the Event date track came out 114px on the header and 133px on the first row, and every figure
   below sat off its own heading. Fixed tracks alone did not save it because each row resolved its
   own `1fr` against its own content box. `display:contents` puts the cells of every row into the
   ONE grid on .mny-table, so a column is a column down the whole page by construction.
   ⚠️ Headers are single-line by design: "Vendor payable", "Balance still owed", "Owed to us" and
   "Acceptance contract" each wrapped to two lines (h=35 measured), which is what pushed the
   headings off their figures. Short labels are the fix, not a wider track. */
/* 🔴 THE TEMPLATE IS IN `rem`, NOT `em`, AND THAT WAS THE WHOLE BUG (measured 2026-09-14).
   Each row is its own grid — which is fine, because every track is a fixed length — but the tracks
   were written in `em` and the header row sets font-size:12px while a body row inherits ~14px. The
   SAME `9.5em` therefore resolved to 114px on the header and 133px on the row, and every figure sat
   off its heading. `rem` resolves against the root, so one template means one set of columns.
   ⚠️ `display:contents` on the rows was tried first and is WRONG here: dissolving <details> also
   dissolves its closed state, so every slip rendered open. Measured, not assumed. */
.mny-table{min-width:820px;font-size:var(--fs-0)}
.mny-row{display:grid;
  /* 🔴 BALANCE GETS ITS OWN WIDTH AND THE CONTRACT LINK IS PUSHED TO THE EDGE (2026-09-14).
     The tracks never overlapped — measured 10px clear at every width from 1000 to 1500 — but a
     RIGHT-aligned figure ending 10px from a LEFT-aligned link reads as a collision, and Balance
     carries the widest values on the page (-$3,230.75, $2,748.95). Balance takes 8.5rem; the
     contract column right-aligns so the link sits on the table's own right edge instead of
     starting immediately after the number. */
  grid-template-columns:9rem minmax(13rem,1fr) repeat(4, 7.5rem) 8.5rem 7rem;
  gap:0 var(--sp-2);align-items:baseline;padding:6px var(--sp-2);
  border-bottom:1px solid var(--line)}
.mny-cell{min-width:0;overflow-wrap:anywhere}

.mny-cell.num{text-align:right;font-variant-numeric:tabular-nums}
.mny-cell.mny-doc{text-align:right}
.mny-row.mny-head{font-size:12px;font-weight:600;color:var(--muted);
  border-bottom:1px solid var(--line);background:var(--card)}
.mny-row.mny-head .mny-cell{white-space:nowrap}
.mny-row.mny-tot{font-weight:650;border-top:2px solid var(--ink);border-bottom:0}
.mny-row.mny-tot .mny-span2{grid-column:span 2}
.mny-row em.muted{font-style:normal}

/* ── THE ROW IS THE CONTROL ──────────────────────────────────────────────────────────────────
   <summary> is the row, so the whole row is the click target and keyboard-operable for free. */
.mny-grp{border:0}
.mny-grp > summary.mny-row{cursor:pointer;list-style:none;margin:0}
.mny-grp > summary.mny-row::-webkit-details-marker{display:none}   /* the native triangle; we draw our own */
.mny-grp > summary.mny-row:hover{background:var(--accent-bg)}
.mny-grp > summary.mny-row:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.mny-grp[open] > summary.mny-row{background:var(--accent-bg);border-bottom-color:var(--accent)}
/* The chevron sits at the LEFT of the row, in the date cell's own gutter, so the date column
   stays aligned with every flat row that has no control. */
.mny-cell.mny-date{display:flex;align-items:baseline;gap:5px;white-space:nowrap}
.mny-row.mny-flat .mny-cell.mny-date,.mny-row.mny-head .mny-cell:first-child,.mny-row.mny-tot .mny-cell:first-child{padding-left:15px}
.mny-chev{flex:0 0 10px;width:10px;height:10px;position:relative;top:1px}
.mny-chev::before{content:"";position:absolute;left:1px;top:1px;width:5px;height:5px;
  border-right:1.6px solid var(--accent);border-bottom:1.6px solid var(--accent);
  transform:rotate(-45deg);transform-origin:60% 60%;transition:transform .12s ease}
.mny-grp[open] > summary .mny-chev::before{transform:rotate(45deg)}
@media (prefers-reduced-motion:reduce){ .mny-chev::before{transition:none} }
/* The movement count: a small number ON the row, never a sentence under it. */
.mny-count{display:inline-block;margin-left:6px;font-size:10.5px;font-weight:600;line-height:1.5;
  min-width:1.45em;padding:0 4px;text-align:center;border-radius:999px;
  background:var(--off);color:var(--muted);vertical-align:1px}
.mny-grp[open] > summary .mny-count{background:var(--accent);color:var(--accent-ink)}

/* ── THE SLIP ────────────────────────────────────────────────────────────────────────────── */
.mny-slip{padding:var(--sp-1) var(--sp-2) var(--sp-2) calc(var(--sp-2) + 15px);
  background:var(--bg);border-bottom:1px solid var(--line)}
table.mny-slipgrid{width:100%;border-collapse:collapse;font-size:12px;
  background:var(--card);border:1px solid var(--line);border-radius:var(--r)}
table.mny-slipgrid th,table.mny-slipgrid td{padding:4px var(--sp-2);text-align:left;vertical-align:top}
table.mny-slipgrid th{font-weight:600;color:var(--muted);border-bottom:1px solid var(--line);white-space:nowrap}
table.mny-slipgrid .num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
table.mny-slipgrid td{border-bottom:1px solid var(--line)}
table.mny-slipgrid tr:last-child td{border-bottom:0}
table.mny-slipgrid tr.voided td{color:var(--muted);text-decoration:line-through}
table.mny-slipgrid tr.voided td .muted{text-decoration:none}

/* ── TEAM: CUSTOM's panel (operator ruling 2026-09-14, round 3) — the four named roles show NO boxes; picking Custom
   reveals the panel, by CSS alone (`:has`, Chrome 105+/Safari 15.4+/Firefox 121+ — no script in the path). A refused
   send re-renders with Custom still checked, so the panel is open with the ticks as posted. */
.role-picker .role-panel{display:none;margin:var(--sp-1) 0 var(--sp-2);padding:var(--sp-2) var(--sp-2) 0;border-left:3px solid var(--line)}
.role-picker:has(input[type=radio][value=custom]:checked) .role-panel{display:block}

/* ══ THE TRUCK CARD — photo-led (operator ruling 2026-09-14) ═══════════════════════════════════
   "The truck's photo as the main element." The card it replaces led with a 40px colour tile and a
   13-control colour picker; the picker moved to /calendar (where the same partial already ran) and
   the tile became the photo. Built from the SAME .bcard the /brands grid uses — the 2026-09-02
   ruling that these are brand cards holds; this adds a photo frame above the body and moves the
   padding off the card so the photo can run to its edges. */

/* 🔴 THE NEUTRAL BADGE, DECLARED (2026-09-14). `.bbadge--hold` has been emitted by three card states
   — "Ready to list", "Waiting on FTA", "Couldn't check" — since the badge classes were written, and
   portal.css declared only --go and --warn. So it rendered as a bare pill: ink on whatever was
   behind it. Beside a 40px tile on a white card nobody noticed; sitting on a PHOTOGRAPH it
   disappears, which is what surfaced it. It is the same token pair the rest of the neutral surfaces
   use, placed beside its two siblings so the set reads as three. */
.bbadge--hold{background:var(--off);color:var(--ink)}

.tcard{padding:0;overflow:hidden;gap:0}
/* 3:2 — the crop a 4:3 phone photo and a 16:9 camera photo both survive, and the shape the grid's
   270px track turns into a ~180px band. aspect-ratio (not a pinned height) so it scales with the
   track at every width. */
.tcard__shot{position:relative;aspect-ratio:3/2;background:var(--off);
  border-bottom:1px solid var(--line);overflow:hidden}
.tcard__shot img{width:100%;height:100%;object-fit:cover;display:block;max-width:100%}
/* their own photo, before we have looked at it — /photos' own treatment, so one photo reads the
   same on both pages */
.tcard__shot img.is-dim{opacity:.72;filter:saturate(.8)}
.tcard__pending{position:absolute;left:0;right:0;bottom:0;background:rgba(26,29,33,.72);color:#fff;
  font-size:11px;font-weight:600;padding:4px var(--sp-2)}
/* the pill sits ON the photo, so it carries its own edge: the fill is opaque (the ink/bg contrast is
   the ruled one and unchanged), and the hairline keeps it off a same-coloured livery */
.tcard__shot .bbadge{position:absolute;left:var(--sp-2);top:var(--sp-2);max-width:calc(100% - var(--sp-4));
  box-shadow:0 1px 2px rgba(0,0,0,.28),0 0 0 1px rgba(255,255,255,.6)}
/* the calendar's colour — read here, chosen on /calendar */
.tcard__dot{position:absolute;right:var(--sp-2);top:var(--sp-2);width:12px;height:12px;border-radius:50%;
  box-shadow:0 0 0 2px rgba(255,255,255,.9),0 1px 2px rgba(0,0,0,.3)}
/* 🔴 THE EMPTY FRAME IS AN INVITATION, NOT A WARNING, AND IT IS QUIETER THAN THE PRIMARY ACTION.
   6 of 12 trucks have no photo and every NEW truck starts here, so this is the commonest state on
   the page — and the truck it will most often appear on is one with six steps outstanding, where
   the photo is the last thing that should be asked for. Faint glyph in the truck's own colour, a
   small quiet link, no fill strong enough to read as a control. */
.tcard__empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;
  justify-content:center;gap:var(--sp-1);text-align:center;padding:var(--sp-2);color:var(--tcolor,var(--muted))}
.tcard__empty svg{display:block;opacity:.5}
.tcard__add{font-size:12px;color:var(--accent);text-decoration:underline}
.tcard__empty--unknown{color:var(--muted)}
.tcard__emptyline{margin:0;font-size:12px;line-height:1.4;color:var(--muted);max-width:22ch}
.tcard__body{display:flex;flex-direction:column;gap:var(--sp-1);padding:var(--sp-3);flex:1;min-width:0}
.tcard__name{font-size:15px;line-height:1.25}
.tcard__where{margin:0;color:var(--muted);font-size:12px;line-height:1.4}
/* 🔴 THE SPACER IS THE EQUAL-HEIGHT MECHANISM HERE, replacing .bcard__desc{flex:1} — this card's
   description is no longer the flexing element (the 2026-09-01 note on .bcard explains why the two
   properties are separate claims). It absorbs the slack so every card's button sits on one line
   however many steps are named above it. */
.tcard__spacer{flex:1;min-height:2px}
/* the blocking reason: cardState's own words, as a line under the name where it can wrap and be a
   link. Red ink, no fill — a fill here would be a second .fmsg on a card that may already have one. */
.bcard__why{margin:2px 0 0;font-size:12.5px;line-height:1.4;color:var(--stop-ink);font-weight:600}
.bcard__why a{color:inherit}
.bcard__n{font-weight:650;color:var(--ink)}
.tcard .bcard__left{font-size:12.5px;color:var(--ink)}
.tcard .bcard__left a{color:var(--muted);text-decoration:underline}
.tcard__switch{margin:0}
.tcard__switch .btn{width:100%}
/* the refusal marks its own card — with five trucks a message alone cannot say which (2026-09-09) */
.tcard--bad{border-color:var(--stop)}
.tcard .fmsg{margin:var(--sp-1) 0 0;font-size:12.5px;padding:var(--sp-1) var(--sp-2)}

/* ══ DASHBOARD — ONE CARD PER TRUCK (2026-09-22, qa/REPORT-2026-09-22-vendor-dashboard-cards.md) ══════════════════════
   🔴 READABLE SIZE, SCOPED TO THE DASHBOARD. The operator ruled the 14px body and 13px truck names too small to read. The
   portal-wide 14px base is the business-tool density every other screen is tuned to, so it is NOT changed here: `.dash`
   raises this one screen to a 17px base, 20px card headings, 16px buttons. 🔴 44px touch targets on every control of this
   screen at every width (the portal's --touch is 32px on desktop). Colours are the portal's own tokens only. */
.dash{font-size:17px;line-height:1.5}
.dash [hidden]{display:none!important}
.dash h1{font-size:28px;margin:var(--sp-3) 0 var(--sp-3)}
.dash .card{padding:var(--sp-3) var(--sp-4)}
.dash .card h2,.dash .dash-h{font-size:20px;font-weight:650;margin:0 0 var(--sp-2)}
.dash .dash-h{margin:var(--sp-4) 0 var(--sp-2)}
.dash .hint{font-size:15px}
.dash .truckline{font-size:17px}
.dash .chip{font-size:14px}
.dash .btn{font-size:16px;min-height:44px;padding:0 var(--sp-3)}
.dash-list{margin:0;padding-left:1.2em}
.dash-list li{margin:0}
.dash-list a{display:block;min-height:44px;padding:10px 0;box-sizing:border-box}
.dash-attn{border-left:6px solid var(--warn)}
/* the cards: one column on a phone, as many ~380px columns as fit beside it; each card is only as tall as it is open */
.dash-cards{display:grid;gap:var(--sp-3);grid-template-columns:1fr;align-items:start}
@media(min-width:900px){.dash-cards{grid-template-columns:repeat(auto-fill,minmax(380px,1fr))}}
.dash-cards .dtc{margin:0}
.dtc__name{font-size:20px;font-weight:700;color:var(--ink);line-height:1.3}
.dtc__head{display:flex;flex-direction:column;gap:var(--sp-2)}
/* 🔴 THE DISCLOSURE IS ONE OF THE PORTAL'S OWN BUTTONS (2026-09-22, third pass). It used to be this:
   a full-width grid with `border:0;background:none`, i.e. a text link wearing a chevron — it did not read
   as the thing you press, and its verb came from a ::before. It is now `btn btn-assist btn-wide dtc__btn`
   and carries NO look of its own beyond its place on the card: the button vocabulary is ruled (2026-08-20/21,
   four kinds four looks, no new ones) and a fifth look defined here is exactly how that ruling erodes.
   🔴 ITS HEIGHT COMES FROM .dtc__btn (44px), NOT FROM --touch, which is 32px on desktop. */
.dtc__toggle{margin-top:var(--sp-1)}
.dtc__count{font-size:16px;font-weight:600;color:var(--go-ink);white-space:nowrap}
.dtc__bar{display:block;-webkit-appearance:none;appearance:none;width:100%;height:12px;border:0;border-radius:999px;
  background:var(--off);overflow:hidden}
.dtc__bar::-webkit-progress-bar{background:var(--off);border-radius:999px}
.dtc__bar::-webkit-progress-value{background:var(--go);border-radius:999px}
.dtc__bar::-moz-progress-bar{background:var(--go);border-radius:999px}
/* 🔴 THE CHEVRON IS GONE FROM THE STYLESHEET, NOT LEFT AS A DEAD CLASS (2026-09-22, third pass). It once
   supplied the words "Show"/"Hide" from a ::before, which put the control's verb where the count it needed
   could not reach it; the whole label is now .dtc__morelabel, rendered by the view and swapped by the
   button itself. `.dtc__more` and `.dtc__toggle--photos` went with it: a finished truck has no disclosure
   at all any more, so a rule for one would style nothing. */
.dtc__morelabel{min-width:0}
/* 🔴 THE "Ready" CARD'S PHOTOS CONTROL (2026-09-22, fourth pass) — a SMALL button on the Ready line, not
   the full-width items disclosure. `.dtc__line .btn{margin-left:auto}` already pushes it to the end of the
   line, so it needs no alignment of its own; what it needs is NOT to inherit the wide button's top margin.
   Its body holds the photos row alone and sits under the line. */
.dtc__toggle--photos{margin-top:0}
.dtc__body--photos{margin-top:var(--sp-2)}
/* the card's own head: name and count on one line, the bar under it, the button under that */
.dtc__line--head{align-items:baseline;min-height:0;margin-bottom:var(--sp-1)}
.dtc__line--head .dtc__count{margin-left:auto}
.dtc__btn{min-height:44px;font-size:16px}
.dtc__body{margin-top:var(--sp-2)}
.dtc__grp{font-size:15px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:var(--sp-3) 0 var(--sp-1)}
.dtc__items{list-style:none;margin:0;padding:0}
.dtc__item{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--sp-1) var(--sp-2);
  padding:var(--sp-2) 0;border-top:1px solid var(--line)}
.dtc__label{flex:1 1 8em;min-width:0}
.dtc__tag{font-size:14px;font-weight:600;color:var(--accent);background:var(--accent-bg);border-radius:999px;padding:4px 10px}
/* a finished truck is ONE line */
.dtc__line{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-1) var(--sp-3);margin:0;min-height:44px}
.dtc__ok{font-weight:650;color:var(--go-ink)}
.dtc__line .btn{margin-left:auto}
.dtc--ready,.dtc--optin{background:var(--go-bg);border-color:var(--go-bg-strong)}
.dash-foot{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2);margin:var(--sp-2) 0 var(--sp-4)}
.dash-grid{gap:var(--sp-3)}
.dash-grid .btn{margin-top:var(--sp-2)}
.dash-docs summary{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--sp-1) var(--sp-2);min-height:44px;cursor:pointer;list-style:none}
.dash-docs summary::-webkit-details-marker{display:none}
.dash-docs summary::before{content:'▸';color:var(--accent);font-size:18px}
.dash-docs[open] summary::before{content:'▾'}
.dash-docs summary h2{margin:0!important}
.dash-docs__sum{color:var(--muted);font-size:16px}
.dash-event{margin-top:var(--sp-4)}
/* ══ THE EDITOR INSIDE A CHECKLIST ROW (2026-09-22) ═══════════════════════════════════════════════════════
   The row keeps its shape — label left, one control right — and the editor opens on its OWN LINE beneath,
   full width, so nothing is squeezed into a flex remainder at 390px. `flex-basis:100%` is what moves it to
   that line; it is inside the <li> (not a sibling) so the row and its editor cannot be separated. */
.dtc-edit{flex:1 1 100%;min-width:0;margin-top:var(--sp-2)}
.dtc-edit[hidden]{display:none!important}
.dtc__item--inline{align-items:flex-start}
.dtc__item--inline .dtc__label,.dtc__item--inline .dtc__btn{align-self:center}
/* ══ THE PHOTOS ROW (2026-09-22) — OPTIONAL, AND IT LOOKS OPTIONAL ════════════════════════════════════════
   It carries no ☐/⏳ rail and no urgency colour: it is not a to-do and must not read as one on a card whose
   whole job is to say what is outstanding. The reason it is worth doing is on the row itself, muted, and the
   count under it is the same sentence /photos prints for this truck. */
.dtc__opt{color:var(--muted);font-weight:400}
.dtc__note{display:block;color:var(--muted);font-size:15px;margin-top:2px}
.dtc__item--photos .dtc__label{font-weight:600}
/* the uploader and the gallery arrive as the wizard's own partial; it is a surface here, not a second card */
.dtc-edit .dtc-photos{background:var(--surface-2,#f7f8fa);border:1px solid var(--line);border-radius:var(--r-2,10px);padding:var(--sp-3)}
.dtc-edit .dtc-photos>p:first-child{margin-top:0}
.dtc-edit .dtc-photos .dropzone{background:var(--bg,#fff)}
/* the editor is a surface, not a second card: one tint, one rule, no shadow competing with the card it is in */
.dtc-edit__form,.dtc-edit__confirm,.dtc-edit .doc-card,.dtc-edit .docbox{
  background:var(--surface-2,#f7f8fa);border:1px solid var(--line);border-radius:var(--r-2,10px);
  padding:var(--sp-3);margin:0}
.dtc-edit .doc-card,.dtc-edit .docbox{box-shadow:none}
.dtc-edit .docbox+.docbox{margin-top:var(--sp-2)}
.dtc-edit h2,.dtc-edit .docbox__h,.dtc-edit__h{font-size:17px;margin:0 0 var(--sp-1)}
.dtc-edit__act{display:flex;flex-wrap:wrap;gap:var(--sp-2);margin-top:var(--sp-3)}
.dtc-edit .btn{min-height:44px;font-size:16px}
.dtc-edit input,.dtc-edit select{font-size:16px;min-height:44px}   /* 16px: iOS zooms a focused field below it */
.dtc-edit label{display:block;margin-top:var(--sp-2)}
.dtc-edit .dims{display:flex;flex-wrap:wrap;gap:var(--sp-2) var(--sp-3)}
.dtc-edit .dims label{flex:1 1 11em;min-width:0;margin-top:0}
.dtc-edit .frow{display:flex;flex-wrap:wrap;gap:var(--sp-2) var(--sp-3)}
.dtc-edit .docrow__file,.dtc-edit .docrow__exp,.dtc-edit .docrow__st{flex:1 1 12em;min-width:0}
.dtc-edit [data-inline-msg]{margin:0 0 var(--sp-2)}
.dtc-edit .field-err{display:block}
/* the vendor-level certificate parks here between visits — never rendered twice (see views/dashboard.php) */
.dtc-edit__hold[hidden]{display:none!important}
/* FIELD LANDING (2026-09-22): a deep link that pins a field's card (/business#f-…, the truck panel's #f-… / #doc-…) stops
   clear of the sticky header — the same offset the truck panel itself pins to (.pt-panel above). */
main .card,main .docbox{scroll-margin-top:calc(var(--touch, 48px) + var(--sp-3, 14px))}

/* ── PRE-AUTH SHELL — the platform brand on every page without a login (operator 2026-09-22) ──────────
   /login looked broken next to foodtruckavenue.com/list-your-truck/, the page a new truck had just left: raw fields, no
   header, grey page. These rules are that page's look, transcribed value for value from its inline sheet and the site
   header (current/templates/list-your-truck.php, site-header.php + the platform's brand_palette): green #173A2B, gold
   #E9B94E, cream #F8EEE7, ink #0F2A1D, Baloo 2 headings, Montserrat body (the fonts arrive via layout.php's pre-auth
   <link>); the card = its signup card (white, 3px green edge, 20px radius, hard 8px shadow); the button = its pill.
   🔴 SCOPED under .auth-page and NEVER via .btn / .card / label / input: the signed-in vocabulary above is untouched, and
   the button gate keeps reading `.btn` as the base it measures. The tokens are literals on purpose — the portal has no
   tenant layer to read a palette from, and the landing page's own fallbacks are these same hexes.
   Touch: inputs 48, buttons 52, the disclosure summary 44 — every control clears the 44px rule on every width. */
.auth-body{background:#F8EEE7}
.auth-page{--au-green:#173A2B;--au-ink:#0F2A1D;--au-cream:#F8EEE7;--au-gold:#E9B94E;--au-gold-hi:#F2C864;--au-gold-text:#7A521A;
  --au-orange:#DF6B3F;--au-white:#fff;
  min-height:100vh;display:flex;flex-direction:column;background:var(--au-cream);color:var(--au-ink);
  font-family:'Montserrat',system-ui,Arial,sans-serif;font-size:18px;line-height:1.55}
.auth-page a{color:var(--au-green);font-weight:700;text-decoration:underline;text-underline-offset:3px}
.auth-page a:hover{color:var(--au-gold-text)}
.auth-page :focus-visible{outline:3px solid var(--au-gold);outline-offset:2px}
/* the bar: the platform header's surface, height and logo cluster, without its nav */
.auth-bar{background:var(--au-green);color:var(--au-white);height:108px;display:flex;align-items:center;padding:0 36px}
/* `.auth-page .auth-brand`, two classes: the page's own `.auth-page a` (green links) outranks a one-class rule, and the
   first render put the white name on the green bar in green — invisible. Measured, not assumed. */
.auth-page .auth-brand{display:inline-flex;align-items:center;gap:12px;text-decoration:none;color:var(--au-white);min-height:44px}
.auth-page .auth-brand:hover{color:var(--au-white)}
.auth-logo{height:78px;width:auto;display:block}
.auth-brand-text{display:flex;flex-direction:column;line-height:1}
.auth-brand-name{font-family:'Baloo 2',system-ui,sans-serif;font-size:24px;font-weight:800;letter-spacing:.01em;white-space:nowrap}
.auth-brand-kicker{margin-top:4px;font-size:10px;font-weight:900;letter-spacing:.16em;text-transform:uppercase;color:var(--au-gold);white-space:nowrap}
/* the page: one centred column; the card is the landing page's signup card */
.auth-main{flex:1;width:100%;max-width:1200px;margin:0 auto;padding:40px 24px 64px}
.auth-card{max-width:560px;margin:0 auto;background:var(--au-white);border:3px solid var(--au-green);border-radius:20px;
  padding:28px 28px 24px;box-shadow:0 8px 0 rgba(23,58,43,.18)}
.auth-page h1{font-family:'Baloo 2',system-ui,sans-serif;font-size:30px;font-weight:800;line-height:1.1;color:var(--au-ink);margin:0 0 .3em}
.auth-lede{font-size:16px;color:var(--au-green);margin:0 0 18px}
.auth-hint{font-size:15px;color:var(--au-green);margin:0 0 14px}
.auth-msg{margin:0 0 16px;padding:12px 14px;border-radius:12px;background:#FBEBDD;color:#6E2B0B;font-weight:600;font-size:16px}
.auth-page .auth-field{display:block;margin:0 0 14px;font-weight:700;font-size:15px}
.auth-page .auth-field span{display:block;margin-bottom:6px}
.auth-page .auth-field input{width:100%;min-height:48px;font:inherit;font-size:17px;font-weight:600;padding:11px 14px;
  border:2px solid var(--au-green);border-radius:12px;background:var(--au-white);color:var(--au-ink)}
.auth-btn{display:inline-flex;align-items:center;justify-content:center;width:100%;min-height:52px;margin-top:6px;
  background:var(--au-gold);color:var(--au-ink);border:3px solid var(--au-green);border-radius:999px;padding:12px 34px;
  font-family:inherit;font-weight:900;font-size:17px;letter-spacing:.06em;text-transform:uppercase;text-decoration:none;
  box-shadow:0 6px 0 var(--au-green);cursor:pointer;transition:transform .16s ease,box-shadow .16s ease,background .16s ease}
.auth-btn:hover{background:var(--au-gold-hi);transform:translateY(-2px);box-shadow:0 8px 0 var(--au-green);color:var(--au-ink)}
.auth-btn:active{transform:translateY(2px);box-shadow:0 3px 0 var(--au-green)}
.auth-btn--alt{background:var(--au-white);color:var(--au-green);box-shadow:0 4px 0 var(--au-green)}
.auth-btn--alt:hover{background:#F3E2D7;color:var(--au-green);box-shadow:0 6px 0 var(--au-green)}
/* the two text links are tap targets too: inline-block with 12px of vertical padding = 44px tall (measured 19px bare) */
.auth-links{margin:6px 0 -6px;font-size:16px;text-align:center}
.auth-links a,.auth-foot a{display:inline-block;padding:12px 4px}
/* the passwordless route: a secondary block behind a disclosure — both routes stay on the page, one Email field shows */
.auth-alt{max-width:560px;margin:22px auto 0;background:var(--au-white);border:2px solid var(--au-green);border-radius:16px;padding:0 22px}
.auth-alt summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:10px;min-height:52px;
  font-weight:700;font-size:16px;color:var(--au-green)}
.auth-alt summary::-webkit-details-marker{display:none}
.auth-alt summary::before{content:'';width:0;height:0;border-left:9px solid var(--au-gold-text);border-top:6px solid transparent;
  border-bottom:6px solid transparent;transition:transform .16s ease;flex:0 0 auto}
.auth-alt[open] summary::before{transform:rotate(90deg)}
.auth-alt form{padding:2px 0 22px}
.auth-foot{max-width:560px;margin:16px auto 0;text-align:center;font-size:17px}
@media (max-width:899.98px){
  .auth-bar{height:80px;padding:0 20px}
  .auth-logo{height:56px}
  .auth-brand-name{font-size:20px}
  .auth-main{padding:24px 16px 48px}
  .auth-card{padding:22px 18px 20px}
  .auth-page h1{font-size:26px}
  .auth-alt{padding:0 16px}
  .auth-btn{padding:12px 16px;font-size:16px}
}

/* ══ VENDOR MARKETPLACE TERMS — the modal + the banner (seat: vendor terms; operator 2026-09-22: "a centered panel over a dimmed page") ══
   views/_terms_modal.php: ~640px wide, at most 80% of the viewport tall, header + summary OUTSIDE the scroll, the text scrolls in
   .tm-body, the footer pinned; on a phone the panel fills the screen with the same header and pinned footer. Own classes (`tm-*`,
   `terms-banner`), never `.onb-strip` / `.card`. No close control: "Not now" is a quiet link, the scrim has no handler. */
body.tm-open{overflow:hidden}
.tm-scrim{position:fixed;inset:0;background:rgba(20,22,26,.55);z-index:900}
.tm{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:640px;max-width:calc(100vw - 32px);max-height:80vh;display:flex;flex-direction:column;background:var(--card);color:var(--ink);border-radius:12px;box-shadow:0 24px 64px rgba(0,0,0,.28);z-index:901;outline:0}
.tm-head{flex:0 0 auto;padding:var(--sp-4) var(--sp-4) var(--sp-3);border-bottom:1px solid var(--line)}
.tm-title{font-size:20px;line-height:1.2;margin:0 0 2px;letter-spacing:-.01em}
.tm-meta{margin:0;font-size:12px;color:var(--muted)}
.tm-summary{margin-top:var(--sp-3);background:var(--bg);border:1px solid var(--line);border-radius:8px;padding:var(--sp-2) var(--sp-3)}
.tm-summary__t{font-weight:700;font-size:13px;margin:0 0 4px}
.tm-summary ul{margin:0 0 4px 18px;padding:0;font-size:13px;line-height:1.45}
.tm-summary li{margin:0 0 2px}
.tm-summary .muted{font-size:12px;margin:0}
.tm-covered{margin-top:var(--sp-3);background:var(--go-bg);border:1px solid var(--go-bg-strong);border-radius:8px;padding:var(--sp-2) var(--sp-3)}
.tm-covered__t{font-weight:700;font-size:13px;margin:0 0 4px;color:var(--go-ink)}
.tm-covered ul{margin:0 0 0 18px;padding:0;font-size:13px;line-height:1.45}
.tm-covered li{margin:0 0 2px}
.tm-summary{margin-top:var(--sp-2)}
.tm-updated{margin-top:var(--sp-3);background:#fff7e6;border:1px solid #f0d9a8;border-radius:8px;padding:var(--sp-2) var(--sp-3);font-size:13px}
.tm-updated__t{font-weight:700;margin:0 0 4px}
.tm-updated p{margin:0 0 6px}
.tm-ok{margin-top:var(--sp-3);background:var(--go-bg);border:1px solid var(--go-bg-strong);border-radius:8px;padding:var(--sp-2) var(--sp-3);font-size:13px}
.tm-ok__t{font-weight:800;font-size:15px;margin:0 0 4px}
.tm-ok p{margin:0 0 4px}
.tm-body{flex:1 1 auto;overflow:auto;padding:var(--sp-3) var(--sp-4);font-size:15px;line-height:1.55;-webkit-overflow-scrolling:touch}
.tm-body h2{font-size:15px;margin:var(--sp-4) 0 var(--sp-1)}
.tm-body h2:first-of-type{margin-top:0}
.tm-body p{margin:0 0 var(--sp-2)}
.tm-intro{font-weight:400}
.tm-foot{flex:0 0 auto;border-top:1px solid var(--line);padding:var(--sp-3) var(--sp-4) calc(var(--sp-3) + env(safe-area-inset-bottom,0px));background:var(--card);border-radius:0 0 12px 12px}
.tm-intent{font-size:12px;font-weight:600;margin:0 0 2px}
.tm-consent{font-size:11px;color:var(--muted);margin:0 0 var(--sp-2)}
.tm-row{display:flex;gap:var(--sp-3);align-items:flex-end;flex-wrap:wrap}
.tm-name{display:flex;flex-direction:column;gap:2px;font-size:11px;font-weight:600;flex:1 1 220px;min-width:0}
.tm-name input{font:italic 17px Georgia,'Times New Roman',serif;padding:4px 6px;border:0;border-bottom:1.5px solid var(--ink);border-radius:0;background:transparent;outline:0;width:100%}
.tm-name input:focus{background:rgba(127,127,127,.06)}
.tm-name.is-bad input{border-bottom-color:var(--stop)}
.tm-agree__btn{min-width:120px;font-size:15px;padding:9px 20px}
.tm-later{background:none;border:0;padding:8px 4px;font:inherit;font-size:13px;color:var(--muted);text-decoration:underline;cursor:pointer}
.tm-later:hover{color:var(--ink)}
.tm-err{color:var(--stop);font-weight:600;font-size:13px;margin:0 0 var(--sp-2)}
.tm-copies__t{font-weight:700;font-size:13px;margin:0 0 4px}
.tm-copies ul{list-style:none;margin:0;padding:0}
.tm-copies li{margin:0 0 6px}
.tm-copy{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:0}
@media (max-width:720px){.tm{left:0;top:0;transform:none;width:100vw;max-width:100vw;height:100vh;height:100dvh;max-height:none;border-radius:0}.tm-head{padding:var(--sp-2) var(--sp-3) var(--sp-2)}.tm-title{font-size:17px}.tm-meta{font-size:11px}.tm-summary{margin-top:var(--sp-2);padding:6px var(--sp-2)}.tm-summary ul{font-size:11.5px;line-height:1.35}.tm-covered{margin-top:var(--sp-2);padding:6px var(--sp-2)}.tm-covered ul{font-size:11.5px;line-height:1.35}.tm-summary .muted{font-size:11px}.tm-body{padding:var(--sp-2) var(--sp-3);font-size:14px}.tm-foot{padding:6px var(--sp-3) calc(6px + env(safe-area-inset-bottom,0px));border-radius:0}.tm-intent{font-size:11px}.tm-consent{font-size:10px;margin-bottom:6px}.tm-agree__btn{min-width:100px;padding:8px 16px}}
.terms-banner{display:flex;gap:var(--sp-2);align-items:center;flex-wrap:wrap;background:#fff7e6;border:1px solid #f0d9a8;color:#5c3d00;border-radius:var(--r);padding:8px 12px;margin:0 0 var(--sp-3);font-size:13px;font-weight:600}
.terms-banner a{color:#5c3d00;text-decoration:underline}
/* DASHBOARD "THIS WEEK" (2026-09-23, qa/REPORT-2026-09-23-this-week-duplicates.md). One line per entry: day · time · title · truck.
   🔴 A long calendar title WRAPS at word boundaries and is never cut mid-word (operator ruling 2026-09-23). The line is plain inline
   flow, not flex, so the browser breaks it only at spaces. A hanging indent keeps a wrapped title visibly one entry.
   `overflow-wrap:break-word` breaks inside a word ONLY when that single word is wider than the whole line (a pasted URL); otherwise
   it never fires. A day with 4 or more entries is a <details> (no JS) whose summary reads "N things on your calendar". */
.truckline.wk{display:block;padding-left:1.25em;text-indent:-1.25em;line-height:1.4;overflow-wrap:break-word;word-break:normal;hyphens:manual}
.wk__t{color:var(--ink);font-variant-numeric:tabular-nums;white-space:nowrap}
.wk__lab{color:var(--ink)}
.wk__nb{white-space:nowrap}   /* a hyphenated word (a phone number, "Cooper-Tacos") never breaks at its hyphen */
.wk__truck{white-space:nowrap}   /* "· Mac N Noodles 1" moves as one piece; a truck name never breaks across lines (measured 09-23 at 390: "Noodles / 1") */
.wk-day>summary{cursor:pointer;list-style:none;min-height:32px}
.wk-day>summary::-webkit-details-marker{display:none}
.wk-day>summary::before{content:'▸';color:var(--accent)}
.wk-day[open]>summary::before{content:'▾'}
.wk-day>.wk{margin-left:var(--sp-3)}

/* ══ THE MESSENGER + THE CHANGE LOG (operator ruling 2026-09-23) — shared by both portals (this sheet is byte-copied to the customer portal at deploy) ══ */
.convo-row{display:block;color:inherit;text-decoration:none}
.convo-row:hover{background:var(--bg)}
.convo-last{font-size:.92em}
.role-chip{font-size:.75em;vertical-align:middle;margin-left:4px}
.convo-list{list-style:none;margin:0 0 var(--sp-3);padding:0;max-height:60vh;overflow-y:auto;display:flex;flex-direction:column;gap:var(--sp-2)}
.convo-day{align-self:center;font-size:.8em;color:var(--muted);margin:var(--sp-2) 0 0}
.convo-day span{background:var(--off);border-radius:12px;padding:2px 10px}
.bubble{max-width:78%;padding:var(--sp-2) var(--sp-3);border-radius:14px;background:var(--off)}
.bubble--sent{align-self:flex-end;background:var(--accent-bg);border-bottom-right-radius:4px}
.bubble--received{align-self:flex-start;border-bottom-left-radius:4px}
.bubble--admin,.bubble--system{align-self:flex-start;background:var(--warn-bg)}
.bubble-who{font-size:.8em;color:var(--muted);margin-bottom:2px}
.bubble-body{white-space:normal;overflow-wrap:anywhere}
.bubble-time{font-size:.75em;text-align:right;margin-top:2px}
.convo-empty{align-self:center}
.convo-head h1{margin-bottom:0}
.changelog{list-style:none;margin:0;padding:0}
.changelog li{display:flex;gap:var(--sp-3);padding:var(--sp-1) 0;border-bottom:1px solid var(--line)}
.changelog li:last-child{border-bottom:0}
.changelog-when{flex:0 0 7.5em;color:var(--muted);font-size:.9em}
