/* ---- the typeface ---------------------------------------------------------
   Self-hosted, and the reasoning is in _shell.html: linking this from
   fonts.googleapis.com made every page send the visitor's IP to a third party,
   including the privacy notice that says none do. Archivo is SIL Open Font
   License, which permits redistribution; the licence is beside the file.

   ONE FILE, FOUR WEIGHTS. Archivo carries a weight axis and Google was serving
   the identical 34KB binary for 400, 500, 600 and 700. Declaring the range lets
   the browser take any weight in it from this one download rather than fetching
   four.

   THE RANGE IS 400 700, NOT 100 900, even though the face carries more. It is a
   promise about what this product asks for, and tests/test_front_end_contract
   enforces the other half of it: nothing may request 650 or 800, because a
   weight the face cannot supply gets SYNTHESISED -- thickened algorithmically,
   smeared, wrong everywhere and never an error. Declaring a wider range than
   the design uses would quietly re-open that door.

   swap, so text paints immediately on the system stack and changes when the
   face arrives. On one bar of signal in a driveway a reflow beats a blank
   screen, which is the same call the old link was making. */
@font-face {
  font-family: Archivo;
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/archivo.woff2") format("woff2-variations");
}

/* ==========================================================================
   SCOPE — tokens and shell
   --------------------------------------------------------------------------
   One file, one place to restyle from. Everything below the token block
   references a token and never a literal.

   The register is deliberate: exact spacing, a real type scale, and motion that
   only ever reports a state change. No glass, no glow, no animated gradient -
   those photograph well and then wash out completely on a phone held at arm's
   length in daylight, which is the only place this gets used. Confidence here
   comes from precision.

   Contrast ratios are measured and carried in the names, so a later edit cannot
   quietly drop one below a floor. Source: docs/ui-spec.md §9.
   ========================================================================== */

:root {
  /* ---- surfaces. Four levels, and the law that goes with them:
     DATA ON WHITE, LABELS ON THE CHASSIS, CHROME ON THE BEZEL.
     The old three spanned 1.03-1.09:1 against one another, which is why
     nothing could sit on anything. ---------------------------------- */
  --bezel:   #001629;  /* 16.74:1 vs chassis. Chrome and hero, never    */
  --chassis: #F2F5F8;  /*          a reading surface.                    */
  --readout: #FFFFFF;  /*  1.094:1 above chassis. Deliberately slight:   */
  --well:    #EDF4FA;  /*  1.110:1 below readout. v3 carries the card    */
                       /*  boundary with elevation and a 20px radius     */
                       /*  rather than a value step, and puts the real   */
                       /*  contrast in a dark hero above it. That is a   */
                       /*  different strategy from the one this file had */
                       /*  and it only works while the shadow and the    */
                       /*  hero are both present -- flatten either and   */
                       /*  you are back to white cards on white.         */

  --on-bezel:       #FFFFFF;  /* 18.31:1 on bezel                       */
  --on-bezel-muted: #A8B4C8;  /*  8.74:1 on bezel, 6.97:1 on the --ink   */
                              /*  hero. v3's own subtitle colour.        */
  --bezel-raised:   #0F3D66;  /* a wash on the bezel, never a boundary.  */
                              /* --on-bezel is 11.17:1 on it.           */
  /* The bezel focus ring's OUTER band. White alone fails the straddle: a
     control at the bar's edge has its outer band landing on the chassis,
     where #FFFFFF measures 1.09:1 and disappears. Composited, this is
     10.98:1 there and 13.96:1 against the white inner band. */
  --ring-halo: rgba(6, 42, 74, .9);

  /* ---- ink ------------------------------------------------------- */
  --ink:   #062A4A;  /* 14.61 readout / 13.35 chassis / 13.16 well     */
  --navy:  #0F3D66;  /* 11.17 / 10.21 / 10.07                          */
  --muted: #5A6473;  /*  5.99 /  5.48 /  5.40. Clears 4.5 on all three */
                     /*  but no longer the 7:1 the old palette held, so */
                     /*  it is body-and-label ink, never a long read.   */

  /* ---- lines. ONE, plus a restricted second. The pale family is
     deleted, not multiplied: a rule is thin and dark, or it is absent
     and space does the work. --------------------------------------- */
  --rule:         #5A6473;  /* 5.99 readout / 5.48 chassis / 5.40 well */
                            /* 3.06 bezel. TEXT-ADJACENT edges only:    */
                            /* form controls, .chip, anything a label   */
                            /* sits against.                            */
  /* SURFACE edges: panels, strips, the light bar's bottom rule. --rule and
     --muted are the same hex, so every boundary in the app was drawn at
     body-text ink strength - roughly twice what WCAG asks of a UI boundary,
     everywhere at once, which is most of the gap between "wireframe" and
     "designed". Measured: 3.60 readout / 3.29 chassis / 3.24 well, all clear
     of the 3:1 non-text floor. The headroom is under 0.3, so this value is
     load-bearing: retint either surface and it has to be re-measured. */
  --rule-soft:    #7E8894;  /* 3.60 readout / 3.29 chassis / 3.24 well */
  --construction: #DDE4EC;  /* 1.28 readout / 1.17 chassis. ROWS INSIDE */
                            /* AN ALREADY-BOUNDED SURFACE ONLY. Never   */
                            /* a control, never a surface edge. Fainter */
                            /* than the old #C2CBC6, which is only safe */
                            /* because v3 separates with space instead. */

  /* ---- orange. HI-VIS FILLS, AMBER MARKS. --hi-vis may only appear
     as background or fill with --ink or --bezel on it; --amber only as
     the border or fill of an indicator of 12px or less; neither is
     ever `color:`. Written this way the rule is enforceable rather
     than remembered, and test_brand_orange_never_carries_information
     passes by construction. ---------------------------------------- */
  --brand:     #FFA128;  /* v3's accent, and the only colour in       */
                         /* scope-logo.svg. --ink on it 7.22:1. But    */
                         /* 2.02:1 on readout and 1.85:1 on chassis,   */
                         /* so it may never be ink or a mark on a      */
                         /* light surface. Every use in v3 sits on     */
                         /* navy or on the bezel, where it is 9.05:1.  */
  --hi-vis:    var(--brand);
  --amber:     #C97A0A;  /* 3.35 readout / 3.06 chassis / 5.47 bezel.  */
                         /* Clears the 3:1 non-text floor on all three, */
                         /* with almost nothing spare on the chassis.   */
  --amber-ink: #A65D00;  /* 5.02 readout / 4.58 chassis - TEXT. v3's    */
                         /* link colour, and the tightest pass here.    */
  --amber-wash: #FFF4E2; /* 1.005:1 on chassis. A wash, never an edge;  */
                         /* the 2px border is what bounds the note.     */
  --error:      #98110F; /* 8.67 readout / 7.92 chassis. Kept: v3 has  */
  --error-wash: #FDF2F2; /* 1.002:1 on chassis                         */
  /* --error at 7%, and the ONLY tint of it. It exists because app.css wrote
     `rgba(152, 17, 15, .07)` longhand for `.errsum a:hover`, which is this
     token spelled out in a form a `grep '#'` audit walks straight past: the
     rule that says no stylesheet but this one carries a value was being
     broken in the one notation nothing checks. A hover fill only, never an
     edge, so it carries no contrast duty of its own.
     ONE CALL SITE, and it is not this partition's to change: `.errsum` is an
     entry-page rule. Whoever takes that block swaps the literal for this. */
  --error-tint: rgba(152, 17, 15, .07);
  --ok-ink:     #0B5F3B; /* 7.74 readout / 7.07 chassis. no opinion on */
  --ok-wash:    #EFF8F3; /* 1.010:1 on chassis    success and failure. */

  /* ---- type ------------------------------------------------------ */
  /* Archivo is v3's voice: a grotesque with a tall x-height and a narrow
     default width, which is what lets a 34px headline hold two words on a
     390px screen. It is SELF-HOSTED now -- see the @font-face at the top of
     this file and the reasoning in _shell.html: the Google Fonts link sent
     every visitor's IP to a third party on the same page that told them
     nothing did. The system stack behind it still renders the first paint. */
  --font: Archivo, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;
  /* Just the stack. This used to lead with "Scope Figure", a family with no
     @font-face anywhere in the repo and no file to load, so every
     `font-family: var(--font-num)` on the figures fell straight through to
     Archivo. A name that resolves to nothing reads as a decision that was
     made; it was not. The figures are tabular because
     font-variant-numeric says so, not because of this. */
  --font-num: var(--font);

  --fs-micro:  0.75rem;    /* 12 - .pill and printed column heads only */
  --fs-meta:   0.875rem;   /* 14 - the outdoor floor                   */
  --fs-body:   1rem;       /* 16                                       */
  --fs-lead:   1.1875rem;  /* 19                                       */
  /* 18, the section-head rung, at last. One rung, six users: h2, #step-4 h2,
     .results-head, .step section > h2, .errsum h2 and .ratefs legend were all
     the same semantic level at six different sizes, and the nearest token to
     v3's 18px was --fs-lead, hand-fitted in a comment that said so.
     h1 pins to 28px on a 390px phone, so this is a 1.56x step there and 1.89x
     on desktop. Against the old 23px it was 1.22x at identical weight and
     identical ink, which on /account produced five near-equal bold lines. */
  --fs-h2:     1.125rem;   /* 18 - the section head                    */
  /* The SECONDARY FIGURE rung. Money that is not the hero total: the step-4
     subtotal, the drawer's credit count, the record page's price. --fs-h2 used
     to carry both jobs, so resizing the heading rung would silently have
     shrunk three money figures. Split, not repurposed. */
  --fs-figure-sm: 1.4375rem;                       /* 23               */
  --fs-h1:     clamp(1.75rem, 6vw, 2.125rem);      /* 28-34            */
  /* 60px is the design's total, and the old 12vw could not reach its own 64px
     cap below a 533px viewport - so the ceiling was unreachable on every phone
     this ships to. 60/390 = 15.4vw lands exactly 60px on the frame the design
     was drawn at, and the 3.75rem cap holds it there on anything wider. */
  --fs-display: clamp(2.875rem, 15.4vw, 3.75rem);  /* 46-60            */

  --lh-tight: 1.1;   --lh-snug: 1.3;   --lh-body: 1.55;
  --track-caps: 0.08em;    --track-tight: -0.01em;
  --track-display: -0.025em; --track-num: -0.015em;
  --w-body: 400; --w-med: 500; --w-bold: 700;

  /* ---- space, form, depth, targets, width ------------------------ */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 20px; --s6: 24px; --s7: 32px; --s8: 40px; --s9: 56px;
  --rhythm: var(--s7); --rhythm-tight: var(--s4);
  --pad-surface: var(--s5); --pad-strip: var(--s3) var(--s4);
  /* The flow column's side gutter, and the ONE figure a full-bleed block may
     cancel with a negative margin. Now --s4, which is what the top bar and the
     trial banner above it have always used: on /new the bar was inset 16px and
     every card under it 14px, and nobody can name 2px but everybody can see
     the cards sticking out past the logo. The old comment defended the 14 as
     "what scope.css's main has always measured", which is a legacy
     measurement preserving itself. A bleed reads the same name its container's
     padding does, and now both names are the same step. */
  --gutter: var(--s4);

  /* The one gap every stacked-card list in the product uses. Seven of them
     shipped at 8, 10 and 12px, three as raw literals, and the design draws
     every one at 10. Page rhythm changing between screens with nothing
     signalling why is exactly what registers as "slightly off". Not --s3:
     10 is the value that was drawn, and one named value beats three near ones. */
  --s-list: 10px;

  --r-sm: 8px; --r: 14px; --r-lg: 20px; --r-pill: 999px;
  /* The radius of a full-bleed dark band. There are three in the product - the
     home hero, step 2's address hero and step 4's estimate hero - and the
     roofer sees two of them within a minute of each other. They shipped at
     30px (raw), 20px and 20px; a 10px corner change on a 390px band, seen
     twice in one session, reads as a bug rather than a style. */
  --r-hero: 30px;
  --bw: 1px; --bw-strong: 2px;

  /* THE FIELD'S FACE. One declaration, three consumers: the home hero, the four
     step heads and the address hero. It starts on the colour every bar above it
     is painted in and ends on --ink, so a navy region is one graduation from the
     top of the screen to the corner where it stops. The worst case for every ink
     on it is its bottom edge: --on-bezel 18.31 falling to 14.61, --on-bezel-muted
     8.74 to 6.97, --brand 9.05 to 7.22, --amber 5.47 to 4.36. Those are the
     figures already recorded against --ink, so nothing on this surface is
     measured against a colour this repo has not measured before.

     Declared HERE and not in scope.css, which is where it was born: scope.css
     loads on /new alone, and app.css's .home-hero is due to read the same name.
     scope.css:1913 already read `var(--field-face, var(--ink))` with a fallback,
     which is the shape of a token that was expected and never arrived - an
     undeclared name means the fallback IS the value, so the gradient silently
     did not happen and nobody could see that it had not. */
  --field-face: linear-gradient(180deg, var(--bezel), var(--ink));

  --shadow-1: 0 1px 2px rgba(6,42,74,.08), 0 2px 8px rgba(6,42,74,.06);
  --shadow-2: 0 2px 8px rgba(6,42,74,.10), 0 10px 26px rgba(6,42,74,.14);
  --shadow-3: var(--shadow-2);   /* name kept, value retired          */

  --tap: 48px; --tap-lg: 56px; --tap-xl: 64px;

  --col: 62rem; --col-narrow: 40rem; --col-flow: 56rem;
  --col-live: var(--col);

  /* ONE MEASURE PER PAGE, and every container that spans the page reads it.

     Four elements used to name their own width -- .topbar-in took --col-live,
     main took --col or .narrow's 40rem, .actionbar-in took --col, .sitefoot
     took --col -- and they disagreed the moment a page was not 62rem wide.
     Measured in Edge at 1440: on /settings main was 640px while the topbar and
     the footer were both 992px, so the logo, the nav and the statutory line all
     ran 176px past the form on each side. On /new main was 896 against a 992
     topbar: 48px out, on the screen every job starts at.

     app.css:759 found this once, for one bar, on one page, and fixed it by
     listing the narrow pages by name. That list was never going to stay
     complete. This is the same fix generalised: set the measure once per page
     and the bug class cannot come back. */
  --page-measure: var(--col);
  --measure: 34rem;

  --t-fast: 120ms; --t: 180ms; --t-slow: 240ms;
  --ease: cubic-bezier(.2, .7, .3, 1);

  --topbar-h: 56px;
  /* No + var(--bw). This reserves the scroll padding the action bar needs, and
     it assumed the bar carried a 1px top border; scope.css sets
     `border-top: 0` on it, so the reservation has always been 1px out. */
  --bar-h: calc(var(--tap-xl) + var(--s3) * 2
                + env(safe-area-inset-bottom, 0px));

  /* The menu drawer. A percentage so a 320px phone keeps a strip of the page
     visible to tap on, capped in rem so a tablet does not get a 600px panel
     holding six rows. */
  --drawer-w: min(83%, 22rem);
  /* What the drawer casts over the page behind it: --bezel at half, which is
     the figure the design draws. Composited over --chassis it darkens the page
     by 3.43:1, which says the page is not the thing in front of you without
     hiding it. It is a shadow rather than an element, so there is no node to
     leave behind and nothing to swallow the tap that closes the panel. Forced
     colours discards it, which is the whole of what decoration may lose.

     Absent, app.css read it as `var(--scrim, transparent)` and the drawer had
     no scrim at all: a three-layer shadow painting two layers. */
  --scrim: rgba(0, 22, 41, .5);
}

/* The alias block. Pointers, not second opinions - and the ONLY reason
   ~200 references in scope.css do not have to move in this commit. */
:root {
  --page: var(--chassis); --card: var(--readout); --sunk: var(--well);
  --inverse: var(--bezel);
  --line: var(--rule); --hair: var(--construction);
  --hair-strong: var(--construction);
  --faint: var(--rule);

  --text-strong: var(--ink); --text-primary: var(--navy);
  --text-muted: var(--muted); --text-accent: var(--amber-ink);
  --orange-500: var(--brand); --orange-700: var(--amber);

  /* THE FOOTPRINT MARKER, and these three are not free choices.
     aerial.py:98-116 proves the constraint: over satellite imagery no single
     flat colour reaches WCAG 1.4.11's 3:1, because wet tarmac (L=0.027) forces
     the stroke light, pale render (L=0.660) forces it dark, and grey concrete
     (L=0.167) rules out everything between. The best any one colour manages is
     1.67:1. So the marker is a PAIR: a light contour that carries the dark
     backgrounds and a dark outline that carries the light ones. Worst case
     3.19:1, and the two are 8.20:1 against each other whatever lies underneath.
     The white halo these replaced is the thing that did not work.
     Duplicated here from Python because the roofer's live overlay draws the
     same marker the server burns into the customer's copy, and the two must be
     the same marker. tests/test_aerial.py asserts these values against
     aerial.py's, so the copy cannot rot. */
  --marker-light: #FFD2A0;   /* the contour; aerial.EDGE_LIGHT */
  --marker-ink:   #3E1F00;   /* the outline; aerial.INK        */
  --marker-body:  #F7931E;   /* the fill;    aerial.GLOW       */
  --surface: var(--readout); --surface-sub: var(--chassis);
  --border: var(--rule);
  --warn-text: var(--amber-ink); --warn-surface: var(--amber-wash);

  --c-ink: var(--ink); --c-navy: var(--navy); --c-muted: var(--muted);
  --c-accent: var(--amber-ink); --c-amber: var(--amber);
  --c-brand: var(--brand); --c-rule: var(--rule);
  --c-hair: var(--construction);
  --s-card: var(--readout); --s-page: var(--chassis);
  --s-sunk: var(--well); --s-warn: var(--amber-wash);

  --target-min: var(--tap); --target-comfort: var(--tap-lg);
  --target-primary: var(--tap-xl);

  --sp-1: var(--s1); --sp-2: var(--s2); --sp-3: var(--s3);
  --sp-4: var(--s4); --sp-5: var(--s6); --sp-6: var(--s7);
  --sp-7: var(--s8);

  /* Two rungs, and they have to stay two.
     scope.css uses these as a deliberate pair -- .svc-name over .svc-basis,
     a column head over its working, a label over its note -- so pointing both
     at --fs-meta flattened seven of those pairs into one size and lost the
     hierarchy they were drawing.

     --t-small carries CONTROL LABELS: .rate-row label over seven identical
     money boxes, .edit-mode label ("Adjust corners" vs "Move whole outline",
     where picking wrong moves the price), .svc-name, .fallback-ask .link-btn,
     table text. Body size, because that is what they were before the alias
     resolved and they are the discriminator between look-alike inputs.

     --t-micro carries NOTES: the disclaimer, the price basis, captions, hints.
     --fs-meta is the documented outdoor floor, so this is the floor and never
     --fs-micro (12px), which is for pills and printed column heads. */
  --t-micro: var(--fs-meta); --t-small: var(--fs-body);
  --t-body: var(--fs-body); --t-lead: var(--fs-lead);
  --t-h2: var(--fs-h2); --t-h1: var(--fs-h1);
  --t-figure: var(--fs-display);
  --fs-small: var(--fs-meta);
  --tracking-caps: var(--track-caps); --tracking-num: var(--track-num);

  --radius: var(--r); --radius-sm: var(--r-sm); --radius-lg: var(--r-lg);
  --border-w: var(--bw); --border-w-strong: var(--bw-strong);
  --elev: var(--shadow-1); --elev-lg: var(--shadow-2);
  --lift: var(--shadow-1); --lift-lg: var(--shadow-2);
  --step: var(--t-slow);
  --f-ui: var(--font); --f-num: var(--font-num);
}

*, *::before, *::after { box-sizing: border-box; }

/* The hidden attribute must win. It works through `display: none` in the UA
   stylesheet, so ANY author display rule beats it - and every button here is a
   flex container for centring. This is why controls appeared on steps they did
   not belong to. `hidden` is not a hint. */
[hidden] { display: none !important; }

html {
  -webkit-text-size-adjust: 100%;
  /* 2.4.11 Focus Not Obscured, both ends. Measured at runtime by syncBars(),
     because a wrapped button label or a device inset changes the real height and
     no calc() can know either. */
  /* --top-h is the height syncBars() MEASURES at runtime; --topbar-h is the
     static guess. Preferring the measured one with the static as fallback is
     what makes this right on both kinds of page: /new runs syncBars(), the
     rest do not, and the static 56px understates a bar that is really 44px of
     nav plus padding plus a border plus the device inset. Reading only
     --topbar-h — as this did — meant every page except /new scrolled a focused
     field to a position partly under the sticky header. */
  scroll-padding-top: calc(var(--top-h, var(--topbar-h)) + 12px);
  /* --run-h is the quote builder's sticky running total, a SECOND bottom
     obstruction that this reservation used to ignore - so a field focused near
     the foot of a long list parked underneath the very figure it was changing.
     Measured at runtime by quote.js and 0px on every other screen. 2.4.11. */
  scroll-padding-bottom: calc(var(--bar-h, var(--tap-lg))
                            + var(--run-h, 0px) + 12px);
}

body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--navy);
  background: var(--page);
  overflow-x: hidden;            /* one long address must not shift the page */
  overscroll-behavior-y: none;
  -webkit-tap-highlight-color: rgba(14, 46, 69, .1);
  -webkit-font-smoothing: antialiased;
  /* No faked weights or italics, ever.
     A system font stack ships 400, 500/600 and 700. Asking for 650, 750 or 800
     — which eighteen rules in this codebase did — gets none of those: the
     browser SYNTHESISES the weight by algorithmically thickening the real one,
     which smears the letterforms and is a large part of what "looks cheap"
     actually is. It is invisible unless you know to look for it, because it
     never fails, it just renders slightly wrong everywhere.
     With this line a weight the face does not have snaps to the nearest one it
     does, so the next person to write 650 sees it do nothing rather than
     shipping a smeared approximation. */
  font-synthesis-weight: none;
  font-synthesis-style: none;
}

/* 16px minimum on anything focusable, or iOS zooms the whole page on focus and
   does not undo it on blur - which reads as the app breaking. */
input, select, textarea, button { font: inherit; font-size: max(1rem, 16px); }
img, svg { max-width: 100%; }
/* break-word, NOT anywhere. `anywhere` permits a break at any character and
   counts those break points when computing min-content width — so a narrow
   table column shredded "RESULT" into "RE SU LT" and a price into "£1,0 83".
   `break-word` breaks only a word that cannot fit a line on its own, which is
   the long unbroken address this rule was added for. */
h1, h2, h3, p, li, td, th, label, dd { overflow-wrap: break-word; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* Two-tone focus ring: one of the two always contrasts, on white or on navy. */
:focus-visible {
  outline: 2px solid var(--navy);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px rgba(255, 255, 255, .9);
}

/* ==========================================================================
   Shell — the top bar, and the mark
   ========================================================================== */
/* ---- the upper bezel ---------------------------------------------------
   Opaque, and the translucency is deleted rather than tuned. It existed to
   keep the bar legible over scrolling content, which an opaque bar does not
   need — and it was actively broken: rgba(255,255,255,.82) over white content
   resolves to about #FEFEFE, so the "boundary" measured 1.046:1 and there was
   no bar edge at all. That does not read as flat, it reads as broken.

   The 13.25:1 value step against the chassis IS the edge now, which is ten
   times the separation the old 1px hairline carried even on the pages where
   that declaration was valid. */
.topbar {
  position: sticky; top: 0; z-index: 40;
  background: var(--bezel);
  border-bottom: 0;
  box-shadow: var(--shadow-2);
  padding-top: env(safe-area-inset-top, 0px);
  color: var(--on-bezel);
}
.topbar-in {
  max-width: var(--page-measure); margin: 0 auto;
  min-height: var(--topbar-h);
  padding: var(--pad-strip);
  display: flex; align-items: center; gap: var(--s3);
  /* Wrap rather than overflow. Measured at a 360px document width this row
     needed 480px — 120px past the viewport — and `body { overflow-x: hidden }`
     HID that rather than preventing it, so Account was clipped and Sign out was
     entirely off-screen while both stayed focusable. A keyboard user tabbed
     into controls that were not on the screen. */
  flex-wrap: wrap;
  row-gap: var(--s2);
}

/* ---- the wordmark ------------------------------------------------------
   Height drives, width follows. The explicit max-width matters: the global
   `img { max-width: 100% }` fights a fixed height in a narrow container - the
   width gets clamped while the height does not, and the wordmark stretches.
   object-fit is the belt to that braces. */
.lockup { display: inline-flex; align-items: center; text-decoration: none; }
.lockup-logo {
  /* Both dimensions pinned, in the asset's exact ratio. Height alone plus
     width:auto is correct in theory and kept losing to something else; an
     explicit pair cannot be distorted by any rule, in any container.

     960 x 246 = 3.902. The asset is the WORDMARK now, not a stacked lockup, so
     the ratio went back up -- and the number that actually matters held still.
     Nobody reads a bounding box; they read how big SCOPE looks. Under the old
     artwork the SCOPE band was 259 of 346 units inside a 27px box, which put it
     on screen at 20px. Here the band IS the artwork, so 21px of box is 21px of
     SCOPE: the same size, within a pixel, of the thing anyone judges.

     THE TAGLINE IS DELIBERATELY NOT IN THIS ASSET. The supplied artwork reads
     SCOPE over TRADESMAN ESTIMATION TOOL, and measured on the master that
     tagline is 33 of 291 units -- 3.1px in this bar. The previous lockup made
     the same trade for ROOFING at 3.8px and got away with it because ROOFING is
     seven wide letters. Twenty-five characters at 3.1px is not sub-legible
     type, it is a smear, and carrying it would shrink SCOPE to make room for
     something nobody can read. The full lockup is cut and kept beside this one
     as scope-lockup-light/dark.png for surfaces with room for it. */
  height: 21px; width: 82px;
  aspect-ratio: 960 / 246;
  max-width: none;
  object-fit: contain;
  display: block;
}
/* .lockup is a flex item of .topbar-in, so it inherits flex-shrink: 1 and can be
   compressed below its pinned width on a narrow bar. That is the one remaining
   route back to a squashed mark, and the pinned pair cannot stop it on its own. */
/* THE NAMEPLATE.
   The SCOPE mark inside scope-logo.svg is an embedded base64 PNG. Decoded and
   sampled, its ink is #062A4A - not the #001848 this note used to claim - and
   that is 1.25:1 against the bezel, so the wordmark is very nearly invisible
   on the bar, not merely low-contrast. Recolouring somebody's logo is not ours
   to do, so it gets a lit plaque instead. A nameplate on an instrument case is
   exactly this object, and it touches no artwork.
   Measured: the plaque is 18.31:1 on the bezel and the mark is 14.61:1 on the
   plaque. app.css owns the rest of its geometry and both tones' hovers. */
.lockup {
  flex: none;
  background: var(--readout);
  border-radius: var(--r-sm);
  padding: var(--s2) var(--s3);
}

.nav { margin-left: auto; display: flex; gap: var(--s2); align-items: center; }
/* (0,2,1), so the bezel ink beats the bare `.nav a` below at (0,1,1)
   regardless of source order. Which rule happens to come last is not something
   the legibility of the top bar should depend on — this codebase has had five
   separate bugs of exactly that shape. */
.topbar .nav a,
.topbar .linklike {
  display: inline-flex; align-items: center;
  min-height: var(--tap); padding: 0 var(--s3);
  border-radius: var(--r-sm);
  font-size: var(--fs-meta); font-weight: 500;
  color: var(--on-bezel-muted);          /* 9.65:1 on the bezel */
  text-decoration: none; cursor: pointer;
  background: none; border: 0; font-family: inherit;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
@media (hover: hover) {
  .topbar .nav a:hover,
  .topbar .linklike:hover {
    color: var(--on-bezel);              /* 16.85:1 */
    background: var(--bezel-raised);
  }
}
/* The focus ring has to invert inside the bezel: --navy measures 1.20:1 there,
   so the global outline would be invisible and only the white halo would carry
   it. Light ring, dark halo — the same two-tone idea, the other way up. */
.topbar a:focus-visible,
.topbar button:focus-visible {
  outline: var(--bw-strong) solid var(--on-bezel);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px var(--ring-halo);
}
.nav a {
  display: inline-flex; align-items: center;
  min-height: var(--tap); padding: 0 var(--s3);
  border-radius: var(--r-sm);
  font-size: var(--fs-meta); font-weight: 600;
  color: var(--muted); text-decoration: none;
  transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.nav a:hover { color: var(--ink); background: var(--sunk); }
/* Current page: weight and a rule, never colour alone. */
/* Reserved on EVERY item, so marking the current page cannot shift the row —
   and because border-width survives forced colours where box-shadow does not.
   The old rule used an inset box-shadow, which is discarded entirely in forced
   colours: the one marker telling you where you are, silently gone. */
.topbar .nav a { border-bottom: 3px solid transparent; }

/* Three channels and none of them is hue: ink, weight, and a rule.
   The old rule set `color: var(--ink)`, which measures 1.05:1 on the bezel —
   so the label for the page you are actually on would be the single most
   invisible thing in the bar. */
.topbar .nav a[aria-current="page"] {
  color: var(--on-bezel);              /* 16.85:1, up from 9.65:1 resting */
  font-weight: 700;
  border-bottom-color: var(--amber);   /* 3.64:1 on the bezel, clears 3:1 */
}

.nav a[aria-current="page"] {
  color: var(--ink); font-weight: 700;
  box-shadow: inset 0 -2px 0 var(--amber);
}

main { max-width: 62rem; margin: 0 auto; padding: var(--s6) var(--s4) var(--s9); }
.narrow { max-width: 40rem; }

/* ==========================================================================
   Type
   ========================================================================== */
/* --track-display, not --track-tight. --track-tight is the BODY value, and
   every h1 on home, history, account, billing, settings, login and signup was
   set at it while the estimate flow's heroes ran at -0.02, -0.025 and -0.03em.
   At 28px the difference is 0.56px a letter, about 11px across "Price your
   first roof", between two h1s in the same product. h2 stays on --track-tight;
   one display tracking, one body tracking, and nothing in between. */
h1 { font-size: var(--fs-h1); line-height: var(--lh-tight);
     letter-spacing: var(--track-display); color: var(--ink);
     margin: 0 0 var(--s2); text-wrap: balance; }
h2 { font-size: var(--fs-h2); line-height: 1.25;
     letter-spacing: var(--track-tight); color: var(--ink); margin: 0 0 var(--s3); }
/* --fs-body inline, because --fs-h3 was an alias to --fs-body pretending to be
   a rung of its own. Its two real consumers (.errsum h2, .ratefs legend) are
   section heads and have moved to --fs-h2; the only <h3> any template renders
   is in hq.html, which is internal. A token whose whole definition is another
   token invites exactly the "h3 is a heading size" assumption that set the
   error summary at body size. */
h3 { font-size: var(--fs-body); color: var(--ink); margin: 0 0 var(--s2); }
p { margin: 0 0 var(--s3); }
.lede { font-size: var(--fs-lead); color: var(--muted); margin: 0 0 var(--s6);
        max-width: 44ch; }
.eyebrow {
  font-size: var(--fs-micro); font-weight: 700;
  letter-spacing: var(--track-caps); text-transform: uppercase;
  color: var(--muted); margin: 0 0 var(--s2);
}
.meta { font-size: var(--fs-meta); color: var(--muted); }
.num { font-variant-numeric: tabular-nums; }

/* ==========================================================================
   Surfaces and controls
   ========================================================================== */
.card {
  background: var(--card);
  /* --rule-soft, not --hair. --hair resolves to --construction, and the law for
     that token is stated in capitals at the top of this file: ROWS INSIDE AN
     ALREADY-BOUNDED SURFACE ONLY, never a control, never a surface edge. A card
     edge is a surface edge, and measured it was 1.17:1 against the chassis it
     sits on - not a weak boundary, no boundary - while .searchbar drew the same
     panel edge at 3.29:1. Five otherwise identical white 20px-radius --shadow-1
     panels, two of them bounded and three of them not. */
  border: var(--bw) solid var(--rule-soft);              /* 3.29:1 on chassis */
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-1);
  padding: var(--s5);
}
.card + .card { margin-top: var(--s4); }
.card-head { display: flex; align-items: baseline; gap: var(--s3);
             margin-bottom: var(--s4); }
.card-head h2, .card-head h3 { margin: 0; }
.card-head .meta { margin-left: auto; }
/* IT WRAPS, and only once the reader has asked for type big enough to need it.

   Measured with Page.setFontSizes {standard: 32} - text-only resize, which is
   what 1.4.4 asks for - this row was the only horizontal overflow left anywhere
   in the product: on /history/{id} at 320px, `span.pill.pill-shown` reached
   x = 322.8 against 320px of glass. Nothing in the row can give way. The pill is
   an inline-flex badge over a nowrap word and floors at its own min-content
   (155.2px at 200%), the heading beside it floors at its longest word (118.7px),
   and 155.2 + 12 of gap + 118.7 is more than the 246px card.

   Not unconditional, and that is the whole of why this is two rules rather than
   one declaration. At 100% text on a 320px phone the two items fit with 0.4px
   to spare: h2 shrunk to its 139px min-content, plus 12px of gap, plus a 94.6px
   Shown pill is 245.6 against 246. A blanket flex-wrap therefore restacks the
   heading of the record page at NORMAL text size, on the phone, which is a
   visible design change bought to fix something nobody at that size can see -
   the exact trade app.css:632 already refused for the setup action bar.

   15rem, same number and same arithmetic as app.css:656 and quote.css:212: a
   media-query rem resolves against the browser's DEFAULT font size rather than
   the page's, so this is 240px at 100% text and matches no handset, 360px at
   150% and matches a 320, and 480px at 200% and matches both 320 and 390. The
   breakpoint tracks the reader, not the handset.

   A wrap and not a clip (rule 9), and not a truncation: nothing leaves the row,
   it is given a second line. `align-items: baseline` resolves per flex line, so
   the pill still sits on its own text baseline once it is on one of its own. */
@media (max-width: 15rem) {
  .card-head { flex-wrap: wrap; }
}

/* THE FILL AND THE INK ARE PART OF THE BASE, not part of the modifiers.
   This rule used to carry geometry alone and leave every colour to
   .btn-primary / .btn-secondary / .btn-ghost below, which means a .btn with no
   modifier fell through to the user agent. Measured in headless Chrome on
   index.html's #share-copy, the one bare .btn in the product, and on a probe
   page linking these two stylesheets:

       background-color  rgb(240, 240, 240)   <- Chrome's ButtonFace
       color             rgb(0, 0, 0)         <- Chrome's ButtonText
       border-color      rgba(0, 0, 0, 0)     at 2px
       surface behind    rgb(242, 245, 248)   <- --chassis

   1.041:1 of fill against the page. WCAG 2.2 AA 1.4.11 asks 3:1 of a control's
   boundary, and 1.041:1 is not a weak boundary, it is no boundary: on that
   page the control was a rectangle of grey the roofer had to already know was
   there. Black ink on ButtonFace is the second half of it, the one place in
   the product not drawn in --ink or --navy.

   Fixed here as a class rather than on the one control, because the next bare
   .btn somebody writes gets the same treatment for free. --card, so the
   fall through is a legitimate light control on the product's own surface, and
   --navy so it is the product's own ink. It does NOT make a bare .btn compliant
   on its own, and it is not meant to: the border stays transparent, so a
   control that needs the 3:1 edge still says .btn-secondary out loud. What this
   stops is the browser answering the question.

   The three modifiers all set `background` themselves and are untouched, and
   .btn-ghost's `background: none` still wins on load order. Verified: primary
   rgb(15,61,102), secondary rgb(255,255,255) with a 5.475:1 border, ghost
   transparent, all unmoved. */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s2);
  min-height: var(--tap); padding: 0 var(--s5);
  border-radius: var(--r); border: var(--bw-strong) solid transparent;
  background: var(--card); color: var(--navy);
  font-size: var(--fs-body); font-weight: 600;
  letter-spacing: var(--track-tight);
  cursor: pointer; text-decoration: none;
}
/* Gated, like every other piece of motion in this file and like app.css:219 and
   scope.css:3098 already do it. This block was NOT: measured under the real
   media feature, .home-start, .jobpick-card and .job all dropped to 0s and .btn
   was the only row that did not move - `background, border-color, transform @
   0.12s` in both runs. scope.css's blanket `* { transition: none !important }`
   covered it on /new and only on /new, so the most common control in the
   product was the one control that ignored the setting on every other page. */
@media (prefers-reduced-motion: no-preference) {
  .btn {
    transition: background var(--t-fast) var(--ease),
                border-color var(--t-fast) var(--ease),
                transform var(--t-fast) var(--ease);
  }
}
.btn:active { transform: translateY(1px); }
/* The fill and the border already move 2.44:1 on press, so the press is never
   reported by the transform alone and dropping it costs no state. */
@media (prefers-reduced-motion: reduce) {
  .btn:active { transform: none; }
}
/* `.btn .btn-primary .btn-lg` IS THE PRIMARY BUTTON OF THIS PRODUCT. Stated
   here because it was not stated anywhere, and seven consecutive screens
   disagreed about it. Measured at 390x844, one journey, in order:

     signup            btn btn-primary btn-lg btn-block   358 x 56   r14
     wizard step 1     btn btn-primary btn-lg             358 x 56   r14
     home              home-start                         350 x 64   r20
     /new              primary                            358 x 64   r20
     settings          btn btn-primary                    141 x 48   r14
     account, history  btn btn-primary btn-lg btn-block   316 x 56   r14

   Three heights, two radii and two class families for one job. The sharpest
   seam is one tap wide: /welcome/ready to /new, where "Start pricing" at 56px
   and r14 becomes "Continue" at 64px and r20 with nothing about the work
   having changed. A man who is not interested in software reads that as two
   different products, and he is not wrong about the reason.

   This combination wins on evidence and not on taste: it already carries five
   of the seven, and 56px is --tap-lg, which is the token the rest of the
   product's large targets are cut from. Nothing here changes. The bare
   `.primary` family in scope.css and the one-off in home.html are the ones
   that move, and they belong to the partitions that own those files. */
.btn-lg { min-height: var(--tap-lg); font-size: var(--fs-lead); }
/* --readout and not #fff. Same value today, so nothing moves; the fault is that
   the most-used button in the product was the one surface that would not follow
   the token layer the next time the readout does, and it is exactly the kind of
   literal a `grep --readout` audit walks straight past. The header of this file
   says everything below the token block references a token and never a literal,
   and this was the last place in it that did not. */
.btn-primary { background: var(--navy); border-color: var(--navy);
               color: var(--readout); }
.btn-primary:hover { background: var(--inverse); border-color: var(--inverse); }
.btn-secondary { background: var(--card); border-color: var(--line); color: var(--navy); }
.btn-ghost { background: none; border-color: transparent; color: var(--navy); }
/* Hover is never colour alone. --readout to --well is 1.110:1, which on a
   bright screen is not a weak signal, it is no signal: these two families
   changed fill and nothing else. The border carries it instead - --rule to
   --ink is a 2.44:1 shift - and both families already reserve the border
   width, so nothing moves. (hover: hover) stops a touch device latching the
   state on after a tap, which is the same guard .jobpick-card uses. */
@media (hover: hover) {
  .btn-secondary:hover,
  .btn-ghost:hover { background: var(--sunk); border-color: var(--ink); }
}
.btn[aria-disabled="true"] { opacity: .55; }
.btn svg { width: 18px; height: 18px; flex: none; }
.btn-block { width: 100%; }

.field { display: grid; gap: var(--s2); margin-bottom: var(--s4); }
.field > label { font-size: var(--fs-meta); font-weight: 600; color: var(--ink); }
.field .hint { font-size: var(--fs-small); color: var(--muted); font-weight: 400; }
/* `password` is in this list for a reason that only shows up once there is a
   Show-password control: without it the field falls back to the user agent's
   own box — about 30px tall with a hairline border — and then JUMPS to 44px
   with a real border the instant the toggle flips its type to `text`. A
   control that changes size when you ask to read it is worse than one that was
   never styled. `number` is here for the same reason on the rate screens. */
input[type="text"], input[type="tel"], input[type="email"],
input[type="password"], input[type="number"],
input[type="url"], input[type="search"], textarea, select {
  width: 100%; min-height: var(--tap);
  padding: var(--s2) var(--s3);
  background: var(--card);
  border: var(--bw) solid var(--line);
  border-radius: var(--r);
  color: var(--ink);
  transition: border-color var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease);
}
/* The soft ring is for the mouse. It must NOT take the outline away with it:
   this selector is (0,1,1) and every :focus-visible rule in this file and in
   scope.css is (0,1,0), so `outline: none` here silently outranked all of them
   and no text field, number field or checkbox in the app drew a keyboard focus
   ring at all. What was left was this shadow alone - rgba(14,46,69,.12) over
   white is about 1.25:1, against the 3:1 a focus indicator has to make.
   Tabbing through eight near-identical rate boxes with nothing marking the
   live one is how a rate gets typed into the wrong row. */
input:focus, textarea:focus, select:focus {
  border-color: var(--navy);
  box-shadow: 0 0 0 3px rgba(14, 46, 69, .12);
}
/* Restated at (0,1,1) so the ring outranks the rule above whatever the order. */
input:focus-visible, textarea:focus-visible, select:focus-visible {
  outline: var(--bw-strong) solid var(--navy);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px rgba(255, 255, 255, .9);
}
input[aria-invalid="true"] { border-color: var(--error); border-width: var(--bw-strong); }
textarea { min-height: 96px; resize: vertical; line-height: var(--lh-body); }

/* ==========================================================================
   Empty states. The first thing he sees, so they set the tone: one sentence
   that says what this is for, and exactly one action.
   ========================================================================== */
.empty {
  text-align: center;
  padding: var(--s9) var(--s5);
  /* .card's treatment exactly, because this IS a card and was the only one not
     dressed as one: measured, it had no box-shadow key at all, while .job,
     .searchbar, .svc and .confidence all carry --shadow-1 and .jobpick-card and
     .card-opt label carry --shadow-2. Its dashed edge was --construction, which
     is 1.17:1 on the chassis and is forbidden on a surface edge by the law at
     the top of this file - the same fault .card had, fixed the same way.

     The elevation is NOT the boundary and must not be asked to be one. Composite
     both of --shadow-1's layers over the chassis and the darkest point measures
     1.30:1, which .searchbar's own comment in app.css already records in the same words for .searchbar
     ("no boundary a person could point at - only a tint"), and .searchbar was
     given a --rule-soft border for exactly that reason. So this takes the border
     too, at 3.60:1 against its own white fill and 3.29:1 against the page. */
  border: var(--bw) solid var(--rule-soft);              /* 3.60:1 on the fill */
  border-radius: var(--r-lg);
  background: var(--card);
  box-shadow: var(--shadow-1);
}
.empty h2 { margin-bottom: var(--s2); }
.empty p { color: var(--muted); max-width: 38ch; margin: 0 auto var(--s6); }

/* ==========================================================================
   Motion. Every rule here reports a state change; anything decorative is cut.
   All of it off under prefers-reduced-motion, where the layout must already
   make sense without it.
   ========================================================================== */
@media (prefers-reduced-motion: no-preference) {
  /* Staggered reveal as an estimate resolves: the order in which figures
     become known, which is information rather than flourish. */
  .reveal { animation: rise var(--t-slow) var(--ease) both; }
  .reveal-1 { animation-delay: 0ms; }
  .reveal-2 { animation-delay: 45ms; }
  .reveal-3 { animation-delay: 90ms; }
  .reveal-4 { animation-delay: 135ms; }
  .reveal-5 { animation-delay: 180ms; }
  @keyframes rise {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: none; }
  }
}

/* The skeleton set and its sheen animation were deleted here, and the policy
   comment above them ("Skeletons, never spinners") went with them. Nothing
   rendered any of it: no template and none of the four scripts writes `sk`,
   `sk-text`, `sk-row` or `sk-img`. scope.css:1201 records that the same block
   was already deleted THERE, because "the locator is the wait now", and this
   copy survived that sweep. It also carried a raw #FAFCFD inside a component
   rule, which the header of this file forbids outright.

   The policy went too, deliberately. A documented rule with no implementation
   is what makes the next person add a spinner and believe they are the first
   to think about it. */

/* ==========================================================================
   Bits shared by more than one page
   ========================================================================== */
/* --------------------------------------------------------------------------
   Status badges — draft | shown | sent | won | lost

   `outcome` is the highest-value column in the store, and it was being conveyed
   by hue alone. Measured:

       --error #98110F  vs  --ok-ink #0B5F3B   =  1.12:1
       in greyscale:        #4B4B4B  vs  #535353  =  1.13:1

   WON and LOST were the same shade of grey. Every ink in this file passes 4.5:1
   against white on its own — which is exactly WHY they are all within 2:1 of
   each other, because the palette is luminance-matched by construction. Passing
   the contrast rule for each colour independently and failing it BETWEEN them
   is the characteristic failure of a carefully built token file.

   Three channels now, and none of them is hue:

     1. THE WORD, always. It is the only channel that also survives a screen
        reader, a printout, a photograph and 400% zoom.

     2. THE SILHOUETTE. Five outlines that differ in SHAPE: dashed ring, half
        disc, arrow, tick, cross. Sun desaturates hue long before it destroys an
        edge, so a closed curve against an open line is the crudest difference
        available and therefore the one that survives at 14px on a bright
        driveway. Tick and cross are the won/lost pair deliberately — asymmetric
        two strokes of unequal length against symmetric two of equal length.

     3. THE POLARITY. `won` is the only SOLID badge. That is a LUMINANCE signal,
        which is the last thing sun takes: white on --ok-ink is 7.74:1, and the
        whole block against a `lost` block beside it is 7.06:1 — 6.99:1 even in
        greyscale, against the 1.13:1 it replaces. Plus a border ladder,
        dashed → 1px → 2px, because border-style and border-width are the only
        visual properties forced-colors mode cannot take away.

   Hue is now the fourth and redundant channel. Strip the colour and it reads.

   The washes are DECORATION and nothing more — 1.01:1 against each other,
   1.10:1 against the card. Do not try to fix this component by darkening one; a
   1.1:1 tint is not perceptible outdoors at any hue, and to reach 3:1 between
   two tints one of them has to go dark enough to break its own ink.

   Borders are currentColor by design, so the border can never drift from the
   ink. Tightest case is --muted at 7.04:1 against a 3:1 floor.
   -------------------------------------------------------------------------- */
.pill {
  display: inline-flex; align-items: center; gap: var(--s1);
  /* 24px so a 14px glyph is a shape rather than a smudge. */
  min-height: 24px;
  padding: 3px var(--s2) 3px 6px;
  border-radius: var(--r-pill);
  /* --fs-small, not --fs-micro. 12px was the smallest text on the page and it
     was carrying the most valuable column on it. One point of size buys more
     outdoors than any amount of colour tuning. */
  font-size: var(--fs-small); font-weight: 700; line-height: 1.2;
  /* Caps stay: for a closed set of five short words always paired with a glyph,
     the uniform x-height is what makes this read as a LABEL rather than as
     prose, and tracked caps keep their counters open at this size where tracked
     lowercase would not. The DOM text must stay sentence case — text-transform
     never reaches the accessibility tree, but a literal "WON" in the markup is
     spelled out W-O-N by VoiceOver. */
  letter-spacing: var(--track-caps); text-transform: uppercase;
  white-space: nowrap;
  border: var(--bw) solid currentColor;
}
.pill svg { width: 14px; height: 14px; flex: none; }

/* Nothing has happened yet. The only dashed badge in the set — a broken line
   reads as "not committed" at any size. */
.pill-draft { color: var(--muted); border-style: dashed; }

/* Put in front of the customer on site. A CLOSED curve, set against `sent`'s
   open arrow, because SHOWN and SENT are near-neighbours as words and the glyph
   has to do the separating. This variant did not exist at all: `shown` is in the
   outcome set and rendered as an unstyled fallback. 12.92:1 on --sunk. */
.pill-shown { color: var(--navy); background: var(--sunk); }

/* It left. An OPEN line, and the border steps up to 2px. 6.68:1. */
.pill-sent  { color: var(--amber-ink); background: var(--amber-wash);
              border-width: var(--bw-strong); }

/* The only inverted badge in the app, and the channel doing the real work: one
   solid dark slug in a column of light ones, findable from a metre away with no
   hue at all. Border colour is stated rather than inherited because currentColor
   is white here. Exactly one badge inverts — two dark slugs 1.12:1 apart would
   be the original bug with more ink. */
.pill-won   { color: var(--card); background: var(--ok-ink);
              border-color: var(--ok-ink); border-width: var(--bw-strong); }

.pill-lost  { color: var(--error); background: var(--error-wash);
              border-width: var(--bw-strong); }

/* .rule, .row and .row-end were deleted here. No template and no script renders
   any of the three, and .rule drew its line in --hair, which this file forbids
   on anything but a row inside an already-bounded surface. A generic `.row`
   sitting unused in a shared stylesheet is an invitation to reach for it instead
   of naming the thing being laid out, which is how .logo-row, .rate-line and
   .sum-row each ended up with their own local answer anyway. */
.stack { display: grid; gap: var(--s4); }
.grid-2 { display: grid; gap: var(--s4); }
@media (min-width: 40rem) { .grid-2 { grid-template-columns: 1fr 1fr; } }


/* ==========================================================================
   The nav — one set of markup, two shapes
   --------------------------------------------------------------------------
   Measured: five items in a non-wrapping row need 480px, and the bar is 360px
   on the phone this is used on. Four of the five rendered past the right edge,
   and `body { overflow-x: hidden }` HID that rather than preventing it — so
   they stayed focusable and stayed invisible. A focus ring on something nobody
   can see is the app's only navigation failing quietly.

   Below 48rem it is a disclosure; at and above it, the row it always was. One
   nav, reshaped. Two copies each hidden by a media query would put every link
   in the accessibility tree twice, name the Main landmark twice, and mark
   aria-current on two elements — a screen reader would report ten destinations.

   48rem in rem and not px: this is a TEXT overflow, so the switch has to move
   when the text does.
   ========================================================================== */

/* The panel anchors to this. .nav must not get a position of its own or it
   becomes the containing block and the panel hangs off the button instead. */
.topbar-in { position: relative; }

/* Ships with the `hidden` attribute and carries an author `display` rule, so
   it depends entirely on [hidden] { display: none !important } above. That is
   what keeps it out of the tab order on a page where nav.js never ran. */
.nav-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s2);
  margin-left: auto;
  min-height: var(--tap); min-width: var(--tap);
  padding: 0 var(--s4);
  border: var(--bw-strong) solid var(--on-bezel-muted);  /* 9.65:1 on bezel */
  border-radius: var(--r-sm);
  background: none;
  color: var(--on-bezel);                                /* 16.85:1        */
  /* 16px, not --fs-meta. The file calls 14px the outdoor floor, and this is
     the one control that appears on every page. */
  font-family: inherit; font-size: var(--fs-body); font-weight: 700;
  cursor: pointer;
}
/* 24px glyph. The gap between bars equals the stroke width; below that, glare
   bleed and antialiasing merge them into a smudge. */
.nav-toggle svg { width: var(--s6); height: var(--s6); flex: none; }
.nav-toggle .nav-icon-x { display: none; }

/* Open: the POLARITY inverts, which is the channel that survives sunlight. A
   glyph swap alone has to be RESOLVED, and a 24px shape at arm's length in sun
   is at the edge of resolution. 16.85:1 of state change. */
.nav-toggle[aria-expanded="true"] {
  background: var(--on-bezel);
  color: var(--bezel);
  border-color: var(--on-bezel);
}
.nav-toggle[aria-expanded="true"] .nav-icon-bars { display: none; }
.nav-toggle[aria-expanded="true"] .nav-icon-x { display: block; }

@media (max-width: 47.999rem) {
  /* Unenhanced: no button, so the row simply wraps. Uglier than the panel and
     entirely usable, which is the right direction to fail. */
  .nav { flex-wrap: wrap; }

  .nav-toggle:not([hidden]) + .nav {
    /* ABSOLUTE. Never in flow, never fixed.
       In flow inside a position:sticky header, an open panel makes the sticky
       box about 340px on a 360x640 phone while scroll-padding-top still
       reserves 68px — so every control the browser scrolls into view lands
       completely underneath it. At 400% zoom it is worse: a sticky box taller
       than the viewport cannot be scrolled at all, and the last item in it is
       unreachable by any input. And `fixed` would resolve against the bar
       rather than the viewport, because a sticky ancestor is already the
       containing block. */
    position: absolute;
    top: 100%; left: 0; right: 0;
    z-index: 20;
    display: grid; gap: var(--s2);
    margin: 0;
    /* 8px of clearance: the focus ring paints about 8px outside the box, and
       overflow-y below clips a box-shadow. The same fault already documented
       on .joblist and on .skip. */
    padding: var(--s2);
    /* Opaque, always. Every ratio in this file is measured against a solid
       named surface; a tint would put page content behind the labels and
       behind their focus rings. */
    background: var(--readout);
    border: var(--bw-strong) solid var(--ink);
    /* A two-tone edge. --rule fails against half the palette this can scroll
       over: 1.05:1 on amber, 1.75:1 on the won green, 1.96:1 on the error red.
       An ink border with a white line outside it is 14.61:1 apart, which puts
       a floor of 4.21:1 under the edge against ANY colour beneath it. */
    box-shadow: 0 var(--bw-strong) 0 0 var(--readout), var(--shadow-2);
    /* When it still does not fit — 400% zoom, large system type — it scrolls
       ITSELF rather than growing past the screen. Never overflow: hidden. */
    max-height: calc(100dvh - var(--top-h, var(--topbar-h)) - var(--s7));
    overflow-y: auto;
    /* The one useful half of a scroll lock, without locking anything: the page
       behind does not take over when the panel reaches its end. */
    overscroll-behavior: contain;
  }
  .nav-toggle:not([hidden]) + .nav[hidden] { display: none; }

  /* Full width removes the horizontal axis from the aiming problem, and
     horizontal precision is the first thing a glove takes away. 56px rows: a
     gloved contact patch is 11-16mm and 56px is 14.6mm, so the target edges
     stay visible around the finger.
     The 8px gap is not decorative — the focus ring extends exactly 4px beyond
     each box, so anything tighter makes two neighbouring rings overlap. */
  .nav-toggle:not([hidden]) + .nav a,
  .nav-toggle:not([hidden]) + .nav .linklike {
    display: flex; align-items: center;
    min-height: var(--tap-lg);
    padding: 0 var(--s4);
    /* Reserved on every item, so marking the current page cannot shift the
       row — and because border-width survives forced colours. */
    border: 0; border-left: var(--s1) solid transparent;
    border-radius: 0;
    color: var(--ink);
    font-size: var(--fs-body); font-weight: 500;
    background: none;
  }
  .nav-toggle:not([hidden]) + .nav a:hover,
  .nav-toggle:not([hidden]) + .nav .linklike:hover {
    background: var(--well); color: var(--ink);
  }
  /* A bottom rule reads as a divider in a column, so the mark moves to the
     leading edge. Weight and fill carry it too — never hue alone. */
  .nav-toggle:not([hidden]) + .nav a[aria-current="page"] {
    border-left-color: var(--amber);     /* 3.35:1 on readout, and a border
                                            owes 3:1, not 4.5 */
    font-weight: 700;
    background: var(--well);
  }
  /* Sign out is the one item Back cannot undo, so it gets clearance and a
     visible rule rather than sitting flush under Account. */
  .nav-toggle:not([hidden]) + .nav .nav-signout {
    display: block;
    margin-top: var(--s2); padding-top: var(--s2);
    border-top: var(--bw) solid var(--rule);
  }
  .nav-toggle:not([hidden]) + .nav .linklike { width: 100%; }
}

@media (min-width: 48rem) {
  /* No disclosure up here. display:none rather than the hidden attribute,
     because an attribute cannot be driven by a media query — and this is what
     keeps the button out of the tab order even if nav.js never loads. */
  .nav-toggle { display: none; }
}

@media (forced-colors: active) {
  /* The panel's edge is a background and a shadow, and neither is painted
     here. border-width and border-style are the two things this mode cannot
     take away. */
  .nav-toggle,
  .nav-toggle:not([hidden]) + .nav { border: var(--bw-strong) solid; }
  .nav-toggle[aria-expanded="true"] { background: ButtonText; color: ButtonFace; }
  .nav-toggle svg { stroke: currentColor; fill: none; }
}
