/* =============================================================================
   postman.homes — app design system
   Written from the Pencil source of truth: ../../untitled.pen
   Replaces the old app.css + desktop.css + the 440px `.phone` frame entirely.

   TWO SYSTEMS, DELIBERATELY:
     structure  — IBM Carbon (colour, 8px spacing, sharp corners, grid)
     identity   — postman.homes (Archivo display, uppercase labels, chevrons,
                  colour fields instead of cards)
   Carbon's border-radius is 0, which is why the two agree. We take Carbon's
   tokens and specs, NOT IBM Plex and NOT the React library.

   RULES, in order of how often they get broken:
     1. NO border radius. Anywhere. Sharp corners are the signature.
     2. NO soft shadows. Separation is a 2px ink rule or a change of surface.
     3. Cards are plain white BLOCKS with no border — never outlined boxes.
     4. Section headings are uppercase + a 2px grey rule (--rule-section).
        It was 4px of ink until 2026-08-05; the label carries the meaning and
        the rule only has to say "a new section starts here".
     5. ONE yellow action per screen. Yellow means "the thing you press".
     6. Blue means interactive or selected. Never decorative.
     7. Every size comes from the spacing ladder or the type scale. Never a
        hand-picked px value. The productive scale is --t-12/14/16/18 (Carbon's
        ramp); display sizes (the clamps, the stat numeral) belong to the
        Archivo identity and sit outside it.
     8. Every pressable thing has all five states: rest, hover, ACTIVE (one
        ramp step past hover), focus, disabled. Disabled is the grey disabled
        tokens, never opacity — 40% yellow still reads as pressable.

   The landing page (index.html) is NOT on this system — it keeps the poster
   palette (cornflower/lemon). The landing sells; the app works.
   ========================================================================== */

@import url("https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@62.5..125,400..900&family=Inter:wght@400;500;600;700&display=swap");

:root {
  /* ---- raw palette (Carbon) — never referenced directly in a rule ------- */
  --blue-10:#edf5ff; --blue-20:#d0e2ff; --blue-30:#a6c8ff;
  --blue-60:#0f62fe; --blue-70:#0043ce; --blue-80:#002d9c;
  --yellow-30:#f1c21b; --yellow-40:#d2a106;
  --gray-10:#f4f4f4; --gray-20:#e0e0e0; --gray-30:#c6c6c6; --gray-50:#8d8d8d;
  --gray-60:#6f6f6f; --gray-70:#525252; --gray-100:#161616;
  --green-50:#24a148; --green-60:#198038; --red-60:#da1e28; --white:#ffffff;

  /* ---- semantic (use THESE) -------------------------------------------- */
  --surface:var(--gray-10);            /* page ground                       */
  --surface-raised:var(--white);       /* a block sitting on the ground     */
  --surface-sunken:var(--blue-10);     /* map wells, tinted panels          */
  --surface-field:var(--blue-60);      /* the big colour field (hero)       */
  --surface-inverse:var(--blue-70);    /* deep field, route band            */

  --text-primary:var(--gray-100);
  --text-secondary:var(--gray-70);
  --text-muted:var(--gray-60);
  --text-inverse:var(--white);
  /* Blue 10, NOT Blue 30: muted-on-blue is body text (the count-band
     explainer, tier notes, the sign-in pitch), and Blue 30 on Blue 60 is
     2.9:1 — under WCAG AA. Blue 10 is 4.6:1 on Blue 60, 6.8:1 on Blue 70,
     and still reads a clear step quieter than white. Blue 30 remains fine
     for GRAPHICS on blue (the map legend keeps it). */
  --text-inverse-muted:var(--blue-10);

  --rule:var(--gray-100);              /* the 2px/4px structural rule       */
  --rule-subtle:var(--gray-20);        /* 1px list divider                  */
  --rule-w:2px; --rule-w-strong:4px;
  /* The trailing rule on a section label. It runs the full width of the
     screen, several times per page, and at 4px of ink that is a lot of black
     spent on something that only has to say "a new section starts here" —
     the LABEL carries the meaning (owner review, 2026-08-05). Half the
     thickness, and grey rather than ink, so the rule structures the page
     without competing with the content inside it. */
  --rule-section:var(--gray-50); --rule-section-w:var(--rule-w);

  --action:var(--yellow-30);           /* the one button                    */
  --action-hover:#e0b416;
  --action-active:var(--yellow-40);    /* pressed = one ramp step past hover */
  --action-text:var(--gray-100);
  --select:var(--blue-60);             /* interactive / selected            */
  --select-hover:var(--blue-70);
  --select-active:var(--blue-80);
  --select-text:var(--white);
  --positive:var(--green-50);          /* fills, dots, accent edges          */
  /* Green 60 for green TEXT: Green 50 on white is 3.4:1 — under AA for the
     11–12px DELIVERED labels it gets used at. Green 50 stays on everything
     that is a shape rather than a word. */
  --positive-text:var(--green-60);
  --positive-field:var(--green-60);    /* a green FIELD carrying white text:
                                          white on Green 50 is 3.4:1, on
                                          Green 60 it is 5.0:1 */
  --danger:var(--red-60);
  --motif:var(--yellow-40);            /* route chevrons                    */

  /* disabled — the Carbon disabled tokens, never opacity: a 40%-opacity
     yellow is still yellow, and a pale-yellow button reads as pressable.   */
  --fill-disabled:var(--gray-30);      /* a disabled control's fill/outline */
  --text-disabled:var(--gray-30);      /* disabled text on a light surface  */
  --text-on-disabled:var(--gray-50);   /* text sitting on --fill-disabled   */

  /* ---- spacing ladder — 8px mini-unit (Carbon) -------------------------- */
  --s1:2px;  --s2:4px;  --s3:8px;  --s4:12px; --s5:16px;
  --s6:24px; --s7:32px; --s8:40px; --s9:48px; --s10:64px;

  /* ---- type ------------------------------------------------------------ */
  --font-display:"Archivo","Helvetica Neue",Helvetica,Arial,sans-serif;
  --font:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;

  /* The productive type scale — Carbon's ramp, four steps. Weight and case
     do the differentiating, not half-pixel size changes: before this ramp
     the file had eleven sizes between 10.5 and 18. Anything bigger is
     display and belongs to Archivo (the clamps, the stat numeral).
     Inputs are never under --t-16: below 16px iOS Safari zooms the page on
     focus. */
  --t-12:12px; --t-14:14px; --t-16:16px; --t-18:18px;

  /* ---- motion (Carbon) — micro-interactions 70ms, panels 150/240ms ------ */
  --ease:cubic-bezier(.2,0,.38,.9);    /* productive standard               */
  --dur-fast:70ms; --dur-mod:150ms; --dur-slow:240ms;

  /* ---- layout ---------------------------------------------------------- */
  --gutter:var(--s5);
  --max:1584px;
}

/* fluid page gutter — Carbon: 16px from md, 24px+ at large */
@media (min-width:672px)  { :root { --gutter:var(--s6); } }
@media (min-width:1056px) { :root { --gutter:var(--s7); } }

/* =============================== reset ==================================== */
*,*::before,*::after { box-sizing:border-box; }
/* Rule 1, enforced rather than merely stated. Sharp corners are the signature,
   and nothing opts out — not a third-party map control, not a stylesheet still
   waiting to be rebuilt. */
*,*::before,*::after { border-radius:0 !important; }
html,body { margin:0; padding:0; }
body {
  font-family:var(--font); font-size:var(--t-14); line-height:1.5;
  color:var(--text-primary); background:var(--surface);
  -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
}
button,input,select,textarea { font:inherit; color:inherit; }
a { color:inherit; text-decoration:none; }
img,svg { display:block; max-width:100%; }
:focus-visible { outline:var(--rule-w) solid var(--select); outline-offset:2px; }
.hide { display:none !important; }
.sr { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; }

/* ============================== shell ===================================== */
/* No phone frame. The app IS the page, and the page is fluid. */
.app { min-height:100vh; display:flex; flex-direction:column; background:var(--surface); }

/* The page scrolls, not a pane inside it. The explicit resets matter on
   order.html: legacy.css's .body sets overflow-y:auto and an 18px pad, and an
   overflow ancestor silently kills position:sticky in every descendant.
   ORDER MATTERS: this must stay ABOVE .wrap. Pages write class="body wrap" on
   one element, and `padding:0` declared after .wrap's padding-inline wipes the
   page gutter — content lands flush at x=0 while the header stays inset. */
.body { flex:1; overflow:visible; padding:0; }
.wrap { width:100%; max-width:var(--max); margin:0 auto; padding-inline:var(--gutter); }

/* top bar — one component, every page */
.bar { background:var(--surface-raised); border-bottom:var(--rule-w) solid var(--rule); }
.bar .wrap { display:flex; align-items:center; gap:var(--s4); padding-block:var(--s5); }
.bar .wordmark { font-family:var(--font-display); font-weight:800; font-size:var(--t-18); letter-spacing:-0.02em; }
.bar .spacer { flex:1; }
.bar nav { display:none; gap:var(--s5); }
.bar nav a { font-size:var(--t-14); font-weight:500; color:var(--text-muted); }
.bar nav a.on { color:var(--text-primary); font-weight:600; }
@media (min-width:672px) { .bar nav { display:flex; } }

/* the mark: a square block — never an emoji, never a letter */
.mark { width:28px; height:28px; background:var(--select); display:grid; place-items:center; flex:none; }
.mark svg { width:16px; height:11px; }
.on-field .mark { background:var(--white); }
.on-field .mark svg path { stroke:var(--surface-field); }

.avatar { width:32px; height:32px; flex:none; border:0; cursor:pointer;
  background:var(--rule); color:var(--surface); display:grid; place-items:center;
  font-size:var(--t-12); font-weight:700; }

/* ============================ colour fields =============================== */
/* A hero is a full-bleed field, not a card. It carries the question. */
.field-band { background:var(--surface-field); color:var(--text-inverse); }
.field-band .wrap { padding-block:var(--s7) var(--s6); }
.field-band.deep { background:var(--surface-inverse); }

/* the delivery-route band — the motif doing structural work as a boundary */
.band { height:22px; background:var(--surface-inverse); overflow:hidden; }
.band svg { height:22px; width:100%; display:block; }

/* ================================ type ==================================== */
.display { font-family:var(--font-display); font-weight:800; text-transform:uppercase;
  font-size:clamp(30px,6vw,50px); line-height:1.02; letter-spacing:-0.035em; margin:0; }
.display-sm { font-size:clamp(22px,4vw,30px); }
/* line-height 1.2, not the body's 1.5: an eyebrow is a label, and the extra
   4px of leading pushed every block under the hero off the boards' rhythm. */
.eyebrow { font-size:var(--t-12); font-weight:700; letter-spacing:0.18em; text-transform:uppercase;
  line-height:1.2; margin:0 0 var(--s3); }
.h-block { font-family:var(--font-display); font-weight:800; letter-spacing:-0.02em; line-height:1.1; margin:0; }
.muted { color:var(--text-muted); }
.num { font-variant-numeric:tabular-nums lining-nums; }

/* section label — uppercase + a 2px grey rule running to the end of the row */
.seclabel { display:flex; align-items:center; gap:var(--s4); margin:var(--s6) 0 var(--s4); }
.seclabel > span { font-size:var(--t-12); font-weight:700; letter-spacing:0.16em; text-transform:uppercase; white-space:nowrap; }
.seclabel::after { content:""; flex:1; height:var(--rule-section-w); background:var(--rule-section); order:1; }
.seclabel.quiet > span { color:var(--text-muted); }
.seclabel > span .count { color:var(--text-muted); }
/* a trailing link sits AFTER the rule, so the rule always reads as the underline
   of the label rather than a gap between the label and the link */
.seclabel .more { order:2; flex:none; font-size:var(--t-12); font-weight:600; color:var(--select); white-space:nowrap; }
.seclabel .more:hover { text-decoration:underline; text-underline-offset:3px; }

/* ============================= components ================================= */
/* s6 side padding, not s7: at the Carbon button size (--t-14) the order flow's
   longest label wrapped to two lines inside a 390px action bar. Carbon's own
   spec pads 16; 24 keeps the presence without the wrap. */
.btn { display:inline-flex; align-items:center; justify-content:center; gap:var(--s3);
  border:0; border-radius:0; cursor:pointer; min-height:48px; padding:0 var(--s6);
  font-size:var(--t-14); font-weight:700; letter-spacing:0.09em; text-transform:uppercase;
  transition:background var(--dur-fast) var(--ease); }
.btn-primary { background:var(--action); color:var(--action-text); }
.btn-primary:hover:not([disabled]) { background:var(--action-hover); }
.btn-primary:active:not([disabled]) { background:var(--action-active); }
.btn-secondary { background:transparent; color:var(--text-primary); box-shadow:inset 0 0 0 var(--rule-w) var(--rule); }
.btn-secondary:hover:not([disabled]) { background:var(--surface-raised); }
.btn-secondary:active:not([disabled]) { background:var(--gray-20); }
/* Rule 8: disabled is the grey tokens, not opacity. */
.btn[disabled] { cursor:not-allowed; }
.btn-primary[disabled] { background:var(--fill-disabled); color:var(--text-on-disabled); }
.btn-secondary[disabled] { color:var(--text-disabled); box-shadow:inset 0 0 0 var(--rule-w) var(--fill-disabled); }
.btn-block { width:100%; }

/* An unselected chip is NOT a box. The filter panel puts ~25 controls on one
   screen, and when every one is a 1.5px ink outline they all carry the same
   weight: nothing is figure, nothing is ground, and the selected chip has to
   out-shout two dozen rectangles. Ground is a soft fill; selection is the only
   ink on the screen (owner review, 2026-08-05).

   line-height declared, not inherited: at the body's 1.5 a chip is 42px and the
   Chip components in the pen are 39. 12 + 15 + 12 = 39. */
.chip { display:inline-flex; align-items:center; border:0; cursor:pointer; padding:var(--s4);
  background:var(--surface); color:var(--text-secondary); box-shadow:none;
  font-size:var(--t-12); line-height:1.25; font-weight:600; letter-spacing:0.03em;
  transition:background var(--dur-fast) var(--ease); }
.chip:hover { background:var(--gray-20); color:var(--text-primary); }
.chip:active { background:var(--gray-30); color:var(--text-primary); }
.chip.on { background:var(--select); color:var(--select-text); box-shadow:none; }
.chip.on:hover { background:var(--select-hover); color:var(--select-text); }
.chip.on:active { background:var(--select-active); color:var(--select-text); }
.chip .n { font-weight:500; opacity:.7; margin-left:var(--s2); }

.tag { display:inline-block; padding:var(--s2) var(--s3); background:var(--blue-20); color:var(--blue-70);
  font-size:var(--t-12); font-weight:700; letter-spacing:0.09em; text-transform:uppercase; }
.status { display:inline-flex; align-items:center; gap:var(--s3); padding:var(--s3) var(--s4);
  background:var(--action); color:var(--action-text);
  font-size:var(--t-12); font-weight:700; letter-spacing:0.09em; text-transform:uppercase; }
.status::before { content:""; width:8px; height:8px; background:currentColor; flex:none; }

.field { width:100%; background:var(--surface-raised); color:var(--text-primary); border:0;
  box-shadow:inset 0 0 0 var(--rule-w) var(--rule); padding:var(--s4) var(--s5);
  font-size:var(--t-16); min-height:48px; }
.field::placeholder { color:var(--text-muted); }
.field:focus { outline:none; box-shadow:inset 0 0 0 var(--rule-w) var(--select); }
.field[disabled] { color:var(--text-disabled); box-shadow:inset 0 0 0 var(--rule-w) var(--fill-disabled); cursor:not-allowed; }
.label { display:block; font-size:var(--t-12); font-weight:700; letter-spacing:0.1em;
  text-transform:uppercase; color:var(--text-secondary); margin-bottom:var(--s3); }

/* a card is a white BLOCK — no border, no radius, no shadow. The explicit
   border/shadow resets matter on order.html, which still loads legacy.css
   underneath this file for its not-yet-rebuilt steps. */
.card { background:var(--surface-raised); border:0; box-shadow:none; padding:var(--s6); }
.card + .card { margin-top:var(--s4); }

/* list row — one 1px divider, not a box each */
.row { display:flex; align-items:center; gap:var(--s5); padding:var(--s5) 0;
  border-bottom:1px solid var(--rule-subtle); }
.row .grow { flex:1; min-width:0; }
.row .t { font-size:var(--t-16); font-weight:600; }
.row .m { font-size:var(--t-12); color:var(--text-muted); }
.row .s { font-size:var(--t-12); font-weight:700; letter-spacing:0.09em; text-transform:uppercase; color:var(--positive-text); }

.stat { background:var(--surface-raised); padding:var(--s5); }
.stat .k { font-size:var(--t-12); font-weight:700; letter-spacing:0.14em; text-transform:uppercase; color:var(--text-muted); }
.stat .v { font-family:var(--font-display); font-weight:800; font-size:30px; letter-spacing:-0.03em; margin-top:var(--s2); }

/* inline notification — a 4px accent bar, no box (Carbon) */
.note { display:flex; gap:var(--s4); background:var(--surface-raised);
  border-left:var(--rule-w-strong) solid var(--select); padding:var(--s5); }
.note.ok { border-left-color:var(--positive); }
.note.warn { border-left-color:var(--action); }
.note.err { border-left-color:var(--danger); }
.note .t { font-size:var(--t-14); font-weight:600; }
.note .b { font-size:var(--t-12); color:var(--text-muted); margin-top:var(--s1); }

/* .banner — the inline message slot the page JS writes into (it sets
   className to "banner", "banner err" or "banner info"). Same shape as .note:
   a 4px accent bar, no box. Empty = hidden, so the slot can always be present. */
/* The variants restate background AND colour, not just the accent edge: on
   order.html legacy.css sits underneath this file and its .banner.info (0,2,0)
   would otherwise out-specify .banner (0,1,0) and tint the block. */
.banner { display:none; background:var(--surface-raised); color:var(--text-secondary);
  border-left:var(--rule-w-strong) solid var(--select);
  padding:var(--s4) var(--s5); font-size:var(--t-14); }
.banner.err  { display:block; background:var(--surface-raised); color:var(--text-secondary); border-left-color:var(--danger); }
.banner.info { display:block; background:var(--surface-raised); color:var(--text-secondary); border-left-color:var(--select); }
.banner.ok   { display:block; background:var(--surface-raised); color:var(--text-secondary); border-left-color:var(--positive); }
.banner.warn { display:block; background:var(--surface-raised); color:var(--text-secondary); border-left-color:var(--action); }
.banner:empty { display:none; }
.banner a { font-weight:600; color:var(--select); text-decoration:underline; text-underline-offset:3px; }
.banner a:hover { color:var(--blue-70); }

/* order progress — flat segments */
/* Progress reads the same everywhere: done = ink, current = blue, ahead = grey.
   Yellow stays out of it — on a screen with a yellow commit button, a yellow
   progress segment competes with the thing you are meant to press. */
.steps { display:flex; gap:var(--s1); }
.steps i { flex:1; height:6px; background:var(--rule-subtle); }
.steps i.on { background:var(--rule); }
.steps i.now { background:var(--select); }

.empty { display:flex; gap:var(--s6); align-items:center; background:var(--surface-raised); padding:var(--s7); }
.empty .t { font-family:var(--font-display); font-weight:800; font-size:var(--t-18); text-transform:uppercase; }
.empty .b { font-size:var(--t-14); color:var(--text-muted); margin-top:var(--s3); line-height:1.5; }

/* sticky action bar — the one place a screen commits */
.actionbar { position:sticky; bottom:0; z-index:10; background:var(--surface-raised);
  border-top:var(--rule-w) solid var(--rule); }
.actionbar .wrap { display:flex; align-items:center; gap:var(--s5); padding-block:var(--s4); }

/* ============================ search field ================================ */
/* The doorway control on home. One unit: white field, 2px rule, square yellow
   go. It is the screen's single yellow action, so no other button competes. */
/* colour DECLARED, not inherited: this is a white surface sitting inside
   .field-band, which sets color:var(--text-inverse) for the copy above it.
   Inheriting made the typed address white on white — invisible. */
.searchbar { display:flex; align-items:stretch; background:var(--surface-raised);
  color:var(--text-primary); padding:var(--rule-w);
  box-shadow:inset 0 0 0 var(--rule-w) var(--rule); }
.searchbar:focus-within { box-shadow:inset 0 0 0 var(--rule-w) var(--select); }
/* On a blue band the focus rule IS the band's own colour, so focusing the field
   erased its outline. Keep the ink rule and put the focus signal outside it, in
   white, where the blue reads it. */
.on-field .searchbar:focus-within {
  box-shadow:inset 0 0 0 var(--rule-w) var(--rule), 0 0 0 var(--rule-w) var(--white); }
.searchbar .mag { display:grid; place-items:center; padding-inline:var(--s5) 0; color:var(--text-muted); flex:none; }
/* s4 block padding, not s5: 12 + 24 line + 12 = the 48px field height both Home
   boards draw. At s5 the field came out 56 and pushed the route band off.
   Less the 2px the frame now pads, so the unit still measures 48. */
.searchbar input { flex:1; min-width:0; border:0; outline:none; background:transparent;
  color:inherit; padding:calc(var(--s4) - var(--rule-w)) var(--s5); font-size:var(--t-16); }
.searchbar input::placeholder { color:var(--text-muted); }
/* Labelled, not icon-only: the system's web-idiom rule is "labeled actions",
   and both Home boards label this one (FIND HOMEOWNERS at lg, GO at sm).
   The frame's 2px padding keeps the yellow INSIDE the rule: as a flush child it
   painted over the rule's top, right and bottom, so the block read 4px taller
   than the field beside it and the unit lost its outline on three sides. */
.searchbar .go { flex:none; border:0; cursor:pointer; padding:0 var(--s6);
  background:var(--action); color:var(--action-text); display:grid; place-items:center;
  font-size:var(--t-14); font-weight:700; letter-spacing:0.09em; text-transform:uppercase; white-space:nowrap;
  transition:background var(--dur-fast) var(--ease); }
.searchbar .go:hover { background:var(--action-hover); }
.searchbar .go:active { background:var(--action-active); }
.searchbar .go .long { display:none; }
@media (min-width:560px) { .searchbar .go .long { display:inline; } .searchbar .go .short { display:none; } }

/* ============================== chips ===================================== */
.chips { display:flex; flex-wrap:wrap; gap:var(--s3); }

/* ============================== money ===================================== */
.money { font-family:var(--font-display); font-weight:800; letter-spacing:-0.02em;
  font-variant-numeric:tabular-nums lining-nums; }
.money .nzd { font-family:var(--font); font-size:.55em; font-weight:700;
  letter-spacing:0.09em; color:var(--text-muted); margin-left:var(--s2); }

/* ============================ icon button ================================= */
.iconbtn { width:40px; height:40px; flex:none; display:grid; place-items:center;
  border:0; background:transparent; color:var(--text-primary); cursor:pointer;
  transition:background var(--dur-fast) var(--ease); }
.iconbtn:hover { background:var(--surface); }
.iconbtn:active { background:var(--gray-20); }
.on-field .iconbtn { color:var(--text-inverse); }
.on-field .iconbtn:hover { background:rgba(255,255,255,.16); }
.on-field .iconbtn:active { background:rgba(255,255,255,.26); }

/* ============================= user menu ================================== */
/* Mounted by app.js mountUserMenu() next to the avatar on every authed page.
   A hard-edged white block on a 2px rule — never a floating rounded card. */
/* Padded by the rule width, same reason .searchbar is: the rule is an inset
   box-shadow, and an inset shadow paints UNDER a child's background — a
   full-width item's hover fill ran straight over the left and right rule, and
   so did the divider. The button padding gives the 2px back so the labels stay
   16 from the outer edge. */
.menu { position:absolute; top:calc(100% + var(--s3)); right:0; z-index:40; min-width:232px;
  background:var(--surface-raised); box-shadow:inset 0 0 0 var(--rule-w) var(--rule);
  padding:var(--s3) var(--rule-w); }
.menu button { display:block; width:100%; text-align:left; border:0; background:none; cursor:pointer;
  padding:var(--s4) calc(var(--s5) - var(--rule-w)); font-size:var(--t-14); font-weight:500; color:var(--text-primary);
  transition:background var(--dur-fast) var(--ease); }
.menu button:hover { background:var(--surface); }
.menu button:active { background:var(--gray-20); }
.menu .menu-div { height:1px; background:var(--rule-subtle); margin:var(--s3) 0; }
/* the avatar is the menu's anchor */
.has-menu { position:relative; }

/* ============================== loading =================================== */
/* A rotating SQUARE, not a circle — the system has no round corners, and a
   spinner is not the place to make the one exception. */
.spinner { width:14px; height:14px; flex:none; border:var(--rule-w) solid currentColor;
  border-right-color:transparent; border-bottom-color:transparent;
  animation:spin .7s linear infinite; }
.spinner.spin-ink { border-color:var(--rule); border-right-color:transparent; border-bottom-color:transparent; }
@keyframes spin { to { transform:rotate(360deg); } }

/* skeletons: a flat grey block with a light sweep, sized inline by the caller */
.skel { display:inline-block; background:var(--gray-20); position:relative; overflow:hidden;
  vertical-align:middle; }
.skel::after { content:""; position:absolute; inset:0; transform:translateX(-100%);
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.7),transparent);
  animation:sweep 1.1s ease-in-out infinite; }
@keyframes sweep { 100% { transform:translateX(100%); } }

/* a number that just changed — a single blue flash, no scaling */
.pulse-accent { animation:flash var(--dur-slow) var(--ease); }
@keyframes flash { 0%,100% { color:inherit; } 50% { color:var(--select); } }

@media (prefers-reduced-motion:reduce) {
  *,*::before,*::after { animation-duration:.001ms !important; transition-duration:.001ms !important; }
}

/* ============================== overlay =================================== */
/* A modal is a hard-edged white block on a dimmed ground. Not a rounded sheet
   with a grabber: this is a web app, so it is a dialog, not an iOS sheet.
   Used by the order flow's sign-off editor; letters.html will want it too. */
.overlay { position:fixed; inset:0; z-index:50; display:grid; place-items:center;
  padding:var(--s5); background:rgba(22,22,22,.55);
  opacity:0; pointer-events:none; transition:opacity var(--dur-mod) var(--ease); }
.overlay.show { opacity:1; pointer-events:auto; }
.sheet { background:var(--surface-raised); width:min(440px,100%); max-height:86vh;
  overflow-y:auto; box-shadow:inset 0 0 0 var(--rule-w) var(--rule); }

/* =============================== zones ==================================== */
/* The app never needs 16 real columns — it needs "main + rail" and
   "map | filters". Two named zone shapes, both single-column below lg. */
.zones { display:grid; gap:var(--s6); padding-block:var(--s6); }
@media (min-width:1056px) {
  .zones { grid-template-columns:1fr 320px; gap:var(--s7); padding-block:var(--s7); }
  .zones.split { grid-template-columns:1fr 400px; gap:0; padding-block:0; }
  .zones.split > :first-child { border-right:var(--rule-w) solid var(--rule); }
}

/* ============================ breakpoint utils ============================ */
/* The boards genuinely differ by breakpoint (a back arrow and a page title at
   sm, a wordmark and nav at lg), and authoring both while showing one is
   clearer than restyling one into the other. These only ever REMOVE an
   element, so the kept element keeps whatever display its component gives it.
   Must stay last in the file so they win over the component rules. */
@media (min-width:672px) { .only-sm { display:none; } }
@media (max-width:671px) { .only-lg { display:none; } }
