/* Quotebook — modern SaaS.
 *
 * What makes this register read as "expensive software" rather than a template is
 * not the colour choice, it is four things done consistently:
 *
 *   1. A SPACING RHYTHM. Every gap is a step on one 4px scale. Arbitrary padding is
 *      the single loudest tell of an unfinished interface.
 *   2. LAYERED SHADOWS. Real elevation is two or three stacked shadows with
 *      different blurs — one flat drop-shadow always looks cheap.
 *   3. TIGHT DISPLAY TYPE, LOOSE BODY TYPE. Large text needs negative tracking and
 *      ~1.05 line-height; body text needs 1.6. Using one setting for both is why
 *      hand-rolled CSS reads amateur.
 *   4. STATES FOR EVERYTHING. Hover, focus-visible, active, disabled. Missing focus
 *      rings are both an accessibility failure and an instant polish tell.
 *
 * Numbers still use tabular figures wherever money appears — a column of prices
 * that wanders undermines the one thing this product sells.
 */

:root {
  /* Type: system sans that resolves to SF Pro / Segoe UI Variable / Inter. */
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI Variable Display", "Segoe UI",
          Inter, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", "Cascadia Mono", Menlo, monospace;

  /* Cool neutrals. */
  --bg: #ffffff;
  --bg-sub: #f8fafc;
  --surface: #ffffff;
  --raised: #f1f5f9;
  --line: #e6ebf1;
  --line-strong: #cfd8e3;
  --ink: #0e1726;
  --ink-2: #48566b;
  --ink-3: #7c8ba1;

  /* One accent, plus a companion for gradients only. */
  --brand: #4f46e5;
  --brand-hover: #4338ca;
  --brand-2: #7c6cf5;
  --brand-wash: #eef0fe;
  --brand-ring: rgba(79, 70, 229, .32);

  --ok: #047857;
  --ok-wash: #e7f6f0;
  --flag: #b45309;
  --flag-wash: #fef4e6;
  --stop: #b42318;
  --stop-wash: #fdeceb;

  /* 4px rhythm. Nothing off-scale. */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px; --s5: 20px;
  --s6: 24px; --s7: 32px; --s8: 40px; --s9: 56px; --s10: 72px; --s11: 96px;

  --r-sm: 8px; --r: 12px; --r-lg: 16px; --r-xl: 22px;

  /* Layered elevation. */
  --e1: 0 1px 2px rgba(16,24,40,.05), 0 1px 3px rgba(16,24,40,.06);
  --e2: 0 1px 2px rgba(16,24,40,.05), 0 4px 8px -2px rgba(16,24,40,.07),
        0 12px 20px -8px rgba(16,24,40,.08);
  --e3: 0 2px 4px rgba(16,24,40,.05), 0 12px 24px -6px rgba(16,24,40,.10),
        0 32px 48px -16px rgba(16,24,40,.14);
  --e-brand: 0 1px 2px rgba(79,70,229,.20), 0 8px 20px -6px rgba(79,70,229,.34);

  --t-display: clamp(2.6rem, 1.7rem + 3.6vw, 4.25rem);
  --t-h1: clamp(1.9rem, 1.45rem + 1.8vw, 2.75rem);
  --t-h2: clamp(1.35rem, 1.2rem + .6vw, 1.65rem);
  --t-lede: clamp(1.0625rem, 1rem + .4vw, 1.1875rem);
  --t-body: 0.9688rem;
  /* Two steps above body, for figures rather than prose: a line total on an order
     sheet, and the running total on the builder. Added because rules were already
     referring to --t-lg/--t-xl and an undefined custom property makes the WHOLE
     declaration invalid — font-size:var(--t-xl) silently left the running total at
     body size, and font:700 var(--t-lg)/1 ... dropped weight and size together. */
  --t-lg: 1.0625rem;
  --t-xl: 1.375rem;
  --t-sm: 0.875rem;
  --t-xs: 0.8125rem;
  --t-kicker: 0.75rem;
}

/* DARK IS A DESIGNED LADDER, NOT AN INVERSION.
   Measured before: --bg vs --bg-sub was 1.028:1 and --bg vs --surface 1.074:1, so the
   alternating bands did not read at all and a card was indistinguishable from the page
   behind it. Every step below is >=1.15:1, which is where a surface change becomes
   visible without turning into a border.
   THE TWO BLOCKS BELOW MUST STAY IDENTICAL — one is the OS preference, the other the
   manual override. */
@media (prefers-color-scheme: dark) {
  :root {
    --bg:#060910; --bg-sub:#141c2c; --surface:#1f2839; --raised:#2b3547;
    --line:#39445a; --line-strong:#4d5a73; --ink:#eef2f7; --ink-2:#a7b4c6;
    --ink-3:#8494aa;
    --brand:#7c74f7; --brand-hover:#918bfa; --brand-2:#a89ffb;
    --brand-wash:#1b1c3a; --brand-ring:rgba(124,116,247,.38);
    --ok:#34d399; --ok-wash:#0f2a22; --flag:#fbbf5c; --flag-wash:#33280f;
    --stop:#f87171; --stop-wash:#2c1719;
    /* ELEVATION IN DARK IS A LIGHT EDGE, NOT A SHADOW.
       A shadow works by darkening what is behind it, and on a #060910 page there is
       nothing left to darken — measured, --e3 gave 1.050 contrast one pixel below the
       hero mock in dark against 1.497 in light, i.e. the focal object of the page floated
       with no separation at all. So each level adds an inset hairline along the top edge,
       which is how a raised surface actually catches light, and keeps a soft shadow only
       to seat the element. */
    --e1: inset 0 1px 0 rgba(255,255,255,.055), 0 1px 2px rgba(0,0,0,.6);
    --e2: inset 0 1px 0 rgba(255,255,255,.075), 0 2px 4px rgba(0,0,0,.55),
          0 10px 22px -8px rgba(0,0,0,.7);
    --e3: inset 0 1px 0 rgba(255,255,255,.10), 0 3px 8px rgba(0,0,0,.55),
          0 24px 48px -14px rgba(0,0,0,.8);
    --e-brand: 0 8px 22px -8px rgba(124,116,247,.55);
  }
}
:root[data-theme="dark"]{
    --bg:#060910; --bg-sub:#141c2c; --surface:#1f2839; --raised:#2b3547;
    --line:#39445a; --line-strong:#4d5a73; --ink:#eef2f7; --ink-2:#a7b4c6;
    --ink-3:#8494aa;
    --brand:#7c74f7; --brand-hover:#918bfa; --brand-2:#a89ffb;
    --brand-wash:#1b1c3a; --brand-ring:rgba(124,116,247,.38);
    --ok:#34d399; --ok-wash:#0f2a22; --flag:#fbbf5c; --flag-wash:#33280f;
    --stop:#f87171; --stop-wash:#2c1719;
    /* ELEVATION IN DARK IS A LIGHT EDGE, NOT A SHADOW.
       A shadow works by darkening what is behind it, and on a #060910 page there is
       nothing left to darken — measured, --e3 gave 1.050 contrast one pixel below the
       hero mock in dark against 1.497 in light, i.e. the focal object of the page floated
       with no separation at all. So each level adds an inset hairline along the top edge,
       which is how a raised surface actually catches light, and keeps a soft shadow only
       to seat the element. */
    --e1: inset 0 1px 0 rgba(255,255,255,.055), 0 1px 2px rgba(0,0,0,.6);
    --e2: inset 0 1px 0 rgba(255,255,255,.075), 0 2px 4px rgba(0,0,0,.55),
          0 10px 22px -8px rgba(0,0,0,.7);
    --e3: inset 0 1px 0 rgba(255,255,255,.10), 0 3px 8px rgba(0,0,0,.55),
          0 24px 48px -14px rgba(0,0,0,.8);
    --e-brand: 0 8px 22px -8px rgba(124,116,247,.55);
}
:root[data-theme="light"]{
  --bg:#fff;--bg-sub:#f8fafc;--surface:#fff;--raised:#f1f5f9;--line:#e6ebf1;
  --line-strong:#cfd8e3;--ink:#0e1726;--ink-2:#48566b;--ink-3:#7c8ba1;
  --brand:#4f46e5;--brand-hover:#4338ca;--brand-2:#7c6cf5;--brand-wash:#eef0fe;
  --brand-ring:rgba(79,70,229,.32);--ok:#047857;--ok-wash:#e7f6f0;--flag:#b45309;
  --flag-wash:#fef4e6;--stop:#b42318;--stop-wash:#fdeceb;
  --e1:0 1px 2px rgba(16,24,40,.05),0 1px 3px rgba(16,24,40,.06);
  --e2:0 1px 2px rgba(16,24,40,.05),0 4px 8px -2px rgba(16,24,40,.07),0 12px 20px -8px rgba(16,24,40,.08);
  --e3:0 2px 4px rgba(16,24,40,.05),0 12px 24px -6px rgba(16,24,40,.10),0 32px 48px -16px rgba(16,24,40,.14);
  --e-brand:0 1px 2px rgba(79,70,229,.20),0 8px 20px -6px rgba(79,70,229,.34);
}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
body{margin:0;background:var(--bg);color:var(--ink);
  font:var(--t-body)/1.6 var(--sans);-webkit-font-smoothing:antialiased;
  letter-spacing:-.003em}
a{color:var(--brand);text-decoration:none}
a:hover{text-decoration:underline;text-underline-offset:3px;text-decoration-thickness:1.5px}
h1,h2,h3{font-family:var(--sans);letter-spacing:-.028em;line-height:1.08;margin:0}
h1{font-weight:700}h2{font-weight:700}h3{font-weight:640;letter-spacing:-.018em;line-height:1.3}
p{margin:0}
:focus-visible{outline:3px solid var(--brand-ring);outline-offset:2px;border-radius:var(--r-sm)}

.wrap{max-width:1140px;margin:0 auto;padding:0 var(--s6)}
.narrow{max-width:432px;margin:0 auto;padding:var(--s9) 0 var(--s11)}
.num{text-align:right;font-variant-numeric:tabular-nums lining-nums;white-space:nowrap}
.muted{color:var(--ink-3)}
.strong{font-weight:600}
.kicker{display:inline-block;font:640 var(--t-kicker)/1 var(--sans);
  letter-spacing:.06em;text-transform:uppercase;color:var(--brand)}

/* ---------------------------------------------------------------- nav */
header.bar{position:sticky;top:0;z-index:40;background:color-mix(in srgb,var(--bg) 82%,transparent);
  backdrop-filter:saturate(180%) blur(14px);border-bottom:1px solid var(--line)}
header.bar .wrap{display:flex;align-items:center;gap:var(--s6);height:64px}
.brand{display:flex;align-items:center;gap:9px;color:var(--ink);
  text-decoration:none;flex:0 0 auto;line-height:1}
/* .brand-mark, NOT .mark — THE SIX-TIME BUG.
 *
 * `.mark` was already taken, further down this file, by the "Made with Quotebook" line at
 * the foot of a quote document: `.mark{margin-top:var(--s6);…}`. Naming the header logo
 * `.mark` inherited that 24px TOP MARGIN. Flexbox honours margins, so align-items:center
 * centred the mark *including* the margin — the artwork sat 12.5px below the wordmark and
 * .brand grew from 28px to 52px tall.
 *
 * Every previous attempt adjusted centring: vertical-align, matching box heights, merging
 * both halves into one svg, translateY nudges. None of them touched margin, so none of
 * them could work. The lesson is the cheap one — a two-word class name in a 42KB
 * stylesheet is a global, and this one collided with a component on a different page. */
.brand .brand-mark{display:block;width:28px;height:28px;flex:0 0 auto;margin:0}
/* line-height:1 matters: the default line box is taller than the glyphs, and that extra
   leading is what pushes a wordmark off-centre next to a fixed-height mark.
   MEASURED, because the claim that used to sit here was backwards. It said "the cap sits
   a hair above middle"; pixel-scanning the live page at 8x says the opposite — inside the
   19.5px line box there is 3.000px above the cap and 2.750px below the baseline, so the
   cap midpoint (32.125) sits 0.125px BELOW the line-box midpoint (32.000). That is one
   eighth of a pixel and needs no correction. A translateY(-.5px) was tried and made it
   THREE TIMES worse (+0.375px). No transform is correct; do not add one back on the
   strength of an unmeasured hunch — that hunch is what produced six failed fixes. */
/* 23px, not 19.5px. Alignment was measured at 0.000px after the .mark collision was
   fixed, and it still read as "off" — because the remaining variable was WEIGHT, not
   position. At 19.5px the cap height is 13.75px, so the solid indigo mark was 2.04x the
   text it sat beside; lockups sit at 1.5-1.8x, and past that the block visually
   outweighs the word. 23px puts the cap at 16.22px and the ratio at 1.73. */
.brand .wordmark{display:block;font:700 23px/1 var(--sans);letter-spacing:-.021em;
  color:var(--ink)}
.brand:hover{text-decoration:none}
.bar nav{margin-left:auto;display:flex;align-items:center;gap:var(--s5);font-size:var(--t-sm)}
.bar nav a{color:var(--ink-2);font-weight:520}
.bar nav a:hover{color:var(--ink);text-decoration:none}
.who{color:var(--ink-3);font-size:var(--t-xs)}
form.inline{display:inline}
.link{background:none;border:0;color:var(--ink-2);cursor:pointer;font:inherit;padding:0}
.link:hover{color:var(--ink)}

/* ---------------------------------------------------------------- buttons */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--s2);
  background:var(--brand);color:#fff;border:1px solid transparent;border-radius:var(--r-sm);
  padding:10px var(--s5);font:600 var(--t-sm) var(--sans);letter-spacing:-.006em;
  cursor:pointer;box-shadow:var(--e-brand);white-space:nowrap}
.btn:hover{background:var(--brand-hover);text-decoration:none;transform:translateY(-1px)}
.btn:active{transform:translateY(0)}
.btn:disabled{opacity:.45;cursor:not-allowed;box-shadow:none;transform:none}
/* --line-strong, and on the hero pair specifically a heavier edge: a 1px --line border is
   about 1.3:1 against the page, so the secondary CTA read as text floating beside the
   primary rather than as a second button. */
.btn.ghost{background:var(--surface);color:var(--ink);border-color:var(--line-strong);
  box-shadow:var(--e1)}
.btn.ghost:hover{background:var(--raised);border-color:var(--ink-3)}
.btn.small{padding:7px var(--s4);font-size:var(--t-xs)}
.btn.big{padding:14px var(--s7);font-size:1rem;border-radius:var(--r)}

/* ---------------------------------------------------------------- hero */
/* The hero carries its own gutter and measure because, unlike every .band, it has no
   inner .wrap — and main is now full-bleed on this page. Same numbers as .wrap so the
   hero's content column lines up exactly with every band below it. */
.hero{position:relative;max-width:1140px;margin:0 auto;padding:var(--s11) var(--s6) var(--s8);
  text-align:center}
@media (max-width:400px){ .hero{padding-left:var(--s4);padding-right:var(--s4)} }
/* This painted NOTHING. `inset:-30% 0 auto` resolves the -30% against the hero's own
   height, which computed to -291px, so with height:640px the box ran from heroTop-291 to
   heroTop+349 while the radial's vertical radius (50% of 640 = 320px) put its whole
   visible falloff above the fold of the element. Anchored at the top instead, sized in
   px, and given a stop that actually reaches the content. */
.hero::before{content:"";position:absolute;inset:0 0 auto;height:520px;z-index:-1;
  pointer-events:none;
  background:radial-gradient(62% 62% at 50% 0%,var(--brand-wash) 0%,transparent 70%)}
/* text-wrap:balance evens the lines instead of filling greedily. Measured at 1280px the
   h1 broke [590px, 266px] — a one-word 266px last line under a 590px one, at 68px type,
   as the first thing anyone sees — and the break fell mid-gradient so the .grad span was
   split across two lines with the ramp restarting. balance moves the break to after "in",
   which is where a designer would have put it, and keeps "ninety seconds." whole.
   Unsupported browsers fall back to today's greedy wrap, so there is nothing to lose. */
/* No balance here: the two lines are authored as blocks (see marketing.html) so the
   dimension line on .grad can never be split across fragments. balance remains on h2. */
.hero h1{font-size:var(--t-display);max-width:19ch;margin:0 auto var(--s5)}
/* Block for the line break, fit-content so the box hugs the phrase — otherwise the
   dimension line measures the 847px container instead of the words, which is the wrong
   measurement and the whole point of the device. */
.hero h1 .l1, .hero h1 .grad{display:block;width:fit-content;margin-inline:auto}
/* Section headings are 2-3 lines, which is exactly balance's sweet spot (it is capped at
   ~6 lines by spec, so it is the wrong tool for body copy — hence not applied to .lede).
   Measured widows before: "Three steps, and none of them is data entry." broke
   [613.8, 202.6] and "Priced for a crew, not an enterprise." [449.0, 206.4] — both a 31%
   last line. */
h2, h3, .band-head h2{text-wrap:balance}
.lede, .hero-note{text-wrap:pretty}
.hero h1 .grad{background:linear-gradient(100deg,var(--brand),var(--brand-2));
  -webkit-background-clip:text;background-clip:text;color:transparent}
.lede{font-size:var(--t-lede);color:var(--ink-2);max-width:57ch;line-height:1.58;
  margin:0 auto var(--s6)}
.hero-cta{display:flex;align-items:center;justify-content:center;gap:var(--s3);
  flex-wrap:wrap;margin-bottom:var(--s3)}
.hero-note{color:var(--ink-3);font-size:var(--t-xs)}
.eyebrow{display:inline-flex;align-items:center;gap:var(--s2);background:var(--surface);
  border:1px solid var(--line);border-radius:999px;padding:5px 14px var(--s1) 12px;
  font:560 var(--t-xs) var(--sans);color:var(--ink-2);box-shadow:var(--e1);
  margin-bottom:var(--s5)}
.eyebrow b{color:var(--brand);font-weight:640}

/* product mock — elevated, the focal object of the page */
.mock{margin:var(--s8) auto 0;max-width:960px;background:var(--surface);
  border:1px solid var(--line);border-radius:var(--r-xl);box-shadow:var(--e3);
  overflow:hidden;text-align:left}
.mock-bar{display:flex;align-items:center;gap:var(--s2);padding:var(--s3) var(--s5);
  background:var(--bg-sub);border-bottom:1px solid var(--line)}
.mock-bar i{width:10px;height:10px;border-radius:50%;background:var(--line-strong);display:block}
.mock-bar span{margin-left:var(--s3);font:500 var(--t-xs) var(--sans);color:var(--ink-3)}
.mock-body{display:grid;grid-template-columns:minmax(0,.78fr) minmax(0,1.22fr)}
/* The input pane was measured at 342.75px with 160.75px of DEAD SPACE below the typed
   list, because a grid row stretches both cells to match the taller one — and the priced
   table on the right is always taller than five lines of scrawl. Rather than shrink the
   pane, give the emptiness a job: the caret makes it read as a text field still being
   typed into, which is what the pane is depicting. align-content keeps the list at the
   top instead of letting it float in the middle. */
.mock-in{padding:var(--s5) var(--s6);border-right:1px solid var(--line);
  background:var(--bg-sub);display:grid;align-content:start;gap:var(--s3)}
.mock-in .scrawl::after{content:"";display:inline-block;width:8px;height:1.05em;
  margin-left:2px;vertical-align:-2px;background:var(--brand);
  animation:caret 1.1s steps(1) infinite}
@keyframes caret{50%{opacity:0}}
@media (prefers-reduced-motion:reduce){
  .mock-in .scrawl::after{animation:none;opacity:.6}
}
.mock-out{padding:var(--s5) var(--s6)}
.mock-label{display:block;font:640 var(--t-kicker) var(--sans);letter-spacing:.06em;
  text-transform:uppercase;color:var(--ink-3);margin-bottom:var(--s3)}
.scrawl{font:13.5px/2 var(--mono);color:var(--ink-2);white-space:pre-wrap;margin:0}
.minilines{width:100%;border-collapse:collapse;font-size:var(--t-sm)}
.minilines td{padding:7px 0;border-bottom:1px solid var(--line)}
.minilines tr:last-child td{border-bottom:0}
.minilines .q{color:var(--ink-3);font-variant-numeric:tabular-nums;width:3.6em;
  font-size:var(--t-xs)}
.minilines .amt{font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap;
  font-weight:560}
.minifoot{display:flex;justify-content:space-between;align-items:baseline;
  margin-top:var(--s3);padding-top:var(--s3);border-top:1.5px solid var(--line-strong)}
.minifoot b{font:700 1.3rem var(--sans);letter-spacing:-.02em;
  font-variant-numeric:tabular-nums}
.badge-flag{display:inline-flex;align-items:center;gap:6px;font:560 var(--t-xs) var(--sans);
  color:var(--flag);background:var(--flag-wash);border-radius:999px;padding:5px 12px;
  margin-top:var(--s3)}

/* ---------------------------------------------------------------- sections */
section.band{padding:var(--s11) 0}
section.band.alt{background:var(--bg-sub);border-block:1px solid var(--line)}
.band-head{max-width:660px;margin:0 auto var(--s8);text-align:center}
/* GROUPING BY PROXIMITY. Measured before: kicker->h2 was 17px and h2->lede 16px, so all
   three elements were equidistant and nothing said they were one unit. The kicker is a
   label ON the heading, so it sits tight to it; the deck is a separate sentence, so it
   gets room. Same ratio the hero already uses. */
.band-head h2{font-size:var(--t-h1);margin:var(--s1) 0 var(--s5)}
.band-head .lede{margin-bottom:0}
.cols{display:grid;gap:var(--s5);grid-template-columns:repeat(auto-fit,minmax(258px,1fr))}
.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);
  padding:var(--s6);box-shadow:var(--e1)}
.card:hover{box-shadow:var(--e2);border-color:var(--line-strong)}
.card h3{font-size:1.0625rem;margin-bottom:var(--s2)}
.card p{color:var(--ink-2);font-size:var(--t-sm);line-height:1.65}
.icon{width:38px;height:38px;border-radius:11px;display:grid;place-items:center;
  background:var(--brand-wash);color:var(--brand);margin-bottom:var(--s4);
  font:700 var(--t-sm) var(--sans)}
.icon svg{width:19px;height:19px}

/* pricing */
.plans{display:grid;gap:var(--s5);grid-template-columns:repeat(auto-fit,minmax(252px,1fr));
  align-items:start;max-width:1000px;margin:0 auto}
/* A pricing table's whole job is horizontal comparison, and at 900px the three cards
   measured [443, 421, 398] — 44px of ragged bottoms, with no row lining up across the
   columns. Flex column + margin-top:auto on the button pins every CTA to the same
   baseline whatever the lists do above it, and align-items:stretch (the grid default,
   asserted here so a later change cannot quietly drop it) keeps the cards equal height. */
.plans{align-items:stretch}
.plan{position:relative;display:flex;flex-direction:column;
  background:var(--surface);border:1px solid var(--line);
  border-radius:var(--r-lg);padding:var(--s6);box-shadow:var(--e1)}
.plan ul{flex:1 1 auto}
.plan .btn{margin-top:auto}
.plan.featured{border-color:var(--brand);box-shadow:var(--e2);
  transform:translateY(-6px)}
.plan.featured::after{content:"Early access";position:absolute;top:-11px;left:var(--s6);
  background:var(--brand);color:#fff;font:640 var(--t-xs) var(--sans);
  padding:3px 11px;border-radius:999px;box-shadow:var(--e-brand)}
.plan .tag{font:640 var(--t-xs) var(--sans);color:var(--brand)}
.plan h3{font-size:1.0625rem;margin:var(--s2) 0 var(--s1);color:var(--ink-2);font-weight:520}
.price{font:700 2.4rem var(--sans);letter-spacing:-.035em;line-height:1;
  font-variant-numeric:tabular-nums;margin-bottom:var(--s5)}
.price small{font:500 var(--t-sm) var(--sans);color:var(--ink-3);letter-spacing:0}
.plan ul{list-style:none;padding:0;margin:var(--s5) 0 0;font-size:var(--t-sm);color:var(--ink-2)}
.plan li{padding:6px 0 6px 26px;position:relative}
.plan li::before{content:"";position:absolute;left:0;top:12px;width:15px;height:8px;
  border-left:2px solid var(--brand);border-bottom:2px solid var(--brand);
  transform:rotate(-45deg) scale(.82);border-radius:1px}
.plan li.no{color:var(--ink-3)}
.plan li.no::before{content:"";border:0;border-top:2px solid var(--line-strong);
  transform:none;width:11px;height:0;top:17px;left:2px}
.plan .btn{width:100%;margin-top:var(--s5)}

/* ---------------------------------------------------------------- forms */
.stack{display:grid;gap:var(--s4)}
label{display:grid;gap:6px;font:540 var(--t-sm) var(--sans);color:var(--ink)}
input,select,textarea{font:var(--t-body) var(--sans);padding:10px 13px;width:100%;
  color:var(--ink);background:var(--surface);border:1px solid var(--line-strong);
  border-radius:var(--r-sm);box-shadow:0 1px 2px rgba(16,24,40,.04)}
textarea{font:13.5px/1.95 var(--mono);resize:vertical}
input::placeholder,textarea::placeholder{color:var(--ink-3)}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--brand);
  box-shadow:0 0 0 4px var(--brand-ring)}
label small{font-weight:400;color:var(--ink-3);font-size:var(--t-xs);line-height:1.5}
.row{display:flex;gap:var(--s3);align-items:center}
.row.between{justify-content:space-between;flex-wrap:wrap}
.inline-label{display:flex;align-items:center;gap:var(--s2);font-size:var(--t-sm)}
.inline-label select{width:auto}
.suffix{color:var(--ink-3);font-size:var(--t-sm);white-space:nowrap}
.rowbtn{display:block;width:100%;text-align:left;background:var(--surface);
  border:1px solid var(--line);border-radius:var(--r);padding:14px var(--s4);
  margin-bottom:var(--s2);font:var(--t-body) var(--sans);color:var(--ink);cursor:pointer;
  box-shadow:var(--e1)}
.rowbtn:hover{border-color:var(--brand);box-shadow:var(--e2)}

/* ---------------------------------------------------------------- notices */
.flash,.warn{border-radius:var(--r);padding:var(--s3) var(--s4);margin:var(--s4) 0;
  font-size:var(--t-sm);display:flex;gap:var(--s2);border:1px solid}
.flash{background:var(--brand-wash);color:var(--brand);border-color:var(--brand-ring)}
.warn{background:var(--flag-wash);color:var(--flag);border-color:color-mix(in srgb,var(--flag) 26%,transparent)}
.pill{font:600 var(--t-xs) var(--sans);padding:3px 10px;border-radius:999px;
  background:var(--raised);color:var(--ink-2);white-space:nowrap}
.pill.good{background:var(--ok-wash);color:var(--ok)}
.pill.warn{background:var(--flag-wash);color:var(--flag)}

/* ---------------------------------------------------------------- app */
.estimate{padding:var(--s8) 0 0}
.estimate h1{font-size:var(--t-h1);margin-bottom:var(--s2)}
.estimate .lede{margin:0 0 var(--s5);text-align:left}
.results{margin:var(--s7) 0 var(--s11)}
.results .head{margin-bottom:var(--s4);align-items:center}
.results .head h2{font-size:var(--t-h2)}
table.lines{width:100%;border-collapse:separate;border-spacing:0;font-size:var(--t-sm);
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);
  overflow:hidden;box-shadow:var(--e1)}
table.lines th,table.lines td{padding:11px var(--s4);text-align:left;
  border-bottom:1px solid var(--line);vertical-align:top}
table.lines tbody tr:last-child td{border-bottom:1px solid var(--line)}
table.lines thead th{font:640 var(--t-xs) var(--sans);letter-spacing:.03em;
  color:var(--ink-3);background:var(--bg-sub)}
table.lines td.num,table.lines th.num{font-variant-numeric:tabular-nums lining-nums}
table.lines tbody tr:hover{background:var(--bg-sub)}
.typed{font-family:var(--mono);font-size:var(--t-xs);color:var(--ink-2)}
tr.flagged{background:var(--flag-wash)}
tr.flagged:hover{background:var(--flag-wash)}
tr.notes td{padding-top:0;border-bottom:1px solid var(--line)}
.note{display:inline-flex;align-items:center;font-size:var(--t-xs);color:var(--flag);
  background:var(--surface);border:1px solid color-mix(in srgb,var(--flag) 30%,transparent);
  border-radius:999px;padding:3px 11px;margin:0 6px 5px 0}
tfoot th{font-weight:560;color:var(--ink-2);border-bottom:0;background:var(--surface)}
tfoot tr.grand th{font:700 1.15rem var(--sans);letter-spacing:-.02em;color:var(--ink);
  border-top:1.5px solid var(--line-strong);font-variant-numeric:tabular-nums}
.save{margin-top:var(--s5)}

.stats{display:grid;gap:var(--s4);grid-template-columns:repeat(auto-fit,minmax(184px,1fr));
  margin:var(--s2) 0 var(--s7)}
.stat{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);
  padding:var(--s5);box-shadow:var(--e1);display:grid;gap:6px}
.stat b{font:700 1.85rem var(--sans);letter-spacing:-.03em;
  font-variant-numeric:tabular-nums;line-height:1}
.stat small{font-size:var(--t-xs);color:var(--ink-3)}
.stat .kicker{color:var(--ink-3)}

.split{display:grid;gap:var(--s6);grid-template-columns:minmax(0,1.6fr) minmax(0,1fr);
  align-items:start;margin-bottom:var(--s11)}
.split aside.card h3{margin-bottom:var(--s4)}

.empty{background:var(--surface);border:1.5px dashed var(--line-strong);
  border-radius:var(--r-lg);padding:var(--s9) var(--s6);text-align:center}
.empty h3{font-size:1.0625rem;margin-bottom:var(--s2)}
.empty p{max-width:44ch;margin:0 auto var(--s5);font-size:var(--t-sm);color:var(--ink-2)}

/* document */
.doc{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);
  padding:var(--s8);margin:var(--s6) 0 var(--s11);box-shadow:var(--e2)}
.dochead{display:flex;justify-content:space-between;align-items:flex-start;gap:var(--s6);
  padding-bottom:var(--s5);border-bottom:2px solid var(--brand);margin-bottom:var(--s5)}
.logo{max-height:56px;max-width:220px}
.bizname{font-size:1.3rem}
.docno{font:700 1.05rem var(--sans);letter-spacing:-.02em}
.billto{margin-bottom:var(--s5);font-size:var(--t-sm)}
.mark{margin-top:var(--s6);font-size:var(--t-xs);text-align:right;color:var(--ink-3)}

footer.foot{border-top:1px solid var(--line);background:var(--bg-sub);
  padding:var(--s8) 0 var(--s9);color:var(--ink-3);font-size:var(--t-sm)}
footer.foot p{margin-bottom:var(--s2);max-width:70ch}
footer.foot a{color:var(--ink-2)}

/* 640, not 860. 860 was off the project's own breakpoint scale (400/640/760/900) and it
   cost the entire 641-860 band — which contains 768 and 820, the two commonest iPad
   widths — where the two-column mock collapsed and the input pane became a wide, mostly
   empty box. Two columns hold fine down to 640. */
@media (max-width:640px){
  .split{grid-template-columns:1fr}
  .mock-body{grid-template-columns:1fr}
  .mock-in{border-right:0;border-bottom:1px solid var(--line)}
  .plan.featured{transform:none}
  .hero{padding-top:var(--s9)}
  section.band{padding:var(--s9) 0}
  .doc{padding:var(--s5)}
  table.lines th,table.lines td{padding:10px var(--s3)}
}
@media (prefers-reduced-motion:no-preference){
  .btn,.card,.rowbtn,input,select,textarea,table.lines tbody tr{
    transition:background .15s ease,border-color .15s ease,box-shadow .18s ease,
               transform .15s ease}
}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}

/* ================================================================ mobile
 * Two real problems on a phone, both structural rather than cosmetic:
 *
 *   1. The nav has six links. Below ~760px they overflow the bar, so they collapse
 *      into a drawer driven by a checkbox — no JS, so it works before scripts load
 *      and with them disabled, and there is no flash of an open menu.
 *   2. The estimate table is six columns. Wide content must scroll inside its own
 *      container; the page body must never scroll sideways, which is the single
 *      most common mobile layout failure.
 */

.burger{display:none}
.navtoggle{display:none}
.scroller{overflow-x:auto;-webkit-overflow-scrolling:touch;
  border-radius:var(--r-lg);scrollbar-width:thin}
.scroller table.lines{min-width:560px}

/* One element, so there is nothing to align it against. Height drives the
   width via the viewBox aspect ratio. */
/* Visually hidden, but present for screen readers and crawlers since the
   visible wordmark now lives inside the SVG. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

@media (max-width: 760px) {
  header.bar .wrap{height:58px;gap:var(--s3)}
  .burger{display:grid;gap:4px;margin-left:auto;padding:9px;cursor:pointer;
    border-radius:var(--r-sm);border:1px solid var(--line)}
  .burger span{display:block;width:17px;height:1.5px;background:var(--ink-2);border-radius:2px}
  .burger:hover{background:var(--raised)}
  .bar nav{position:absolute;top:58px;left:0;right:0;margin:0;
    flex-direction:column;align-items:stretch;gap:0;
    background:var(--surface);border-bottom:1px solid var(--line);
    box-shadow:var(--e2);display:none;padding:var(--s2) var(--s4) var(--s4)}
  .navtoggle:checked ~ nav{display:flex}
  .bar nav a,.bar nav form,.bar nav .who{padding:11px 2px;
    border-bottom:1px solid var(--line);font-size:0.9375rem}
  .bar nav a:last-of-type{border-bottom:0}
  .bar nav .btn{margin-top:var(--s3);width:100%}

  /* KEEP THE HORIZONTAL GUTTER. This was `padding: var(--s8) 0 var(--s6)` — correct when
     the hero sat inside main.wrap, which supplied the gutter. Making the marketing page
     full-bleed moved the hero out of that wrapper and gave it its own padding, and this
     rule went on zeroing it: on a real iPhone the CTAs ran edge to edge, touching the
     bezel. Neither the overflow check nor the tap-target check in tools/shots.py can see
     that, which is why it took a photo of a phone to find. */
  .hero{padding:var(--s8) var(--s6) var(--s6)}
  .hero h1{max-width:none}
  .hero-cta{flex-direction:column;align-items:stretch}
  .hero-cta .btn{width:100%}
  .eyebrow{font-size:0.75rem;padding:4px 11px}
  .mock{margin-top:var(--s6);border-radius:var(--r-lg)}
  .mock-in,.mock-out{padding:var(--s4) var(--s5)}
  .scrawl{font-size:12.5px;line-height:1.85}

  .band-head{margin-bottom:var(--s6)}
  .cols,.plans{gap:var(--s4)}
  .card{padding:var(--s5)}
  .stats{grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--s3)}
  .stat{padding:var(--s4)}
  .stat b{font-size:1.5rem}
  .results{margin-bottom:var(--s9)}
  .results .head{flex-wrap:wrap;gap:var(--s3)}

  /* Forms: 16px minimum, or iOS Safari zooms the whole page on focus. */
  input,select,textarea{font-size:16px}
  .row.between{gap:var(--s3)}
  .save{flex-direction:column;align-items:stretch}
  .save .btn,.save .inline-label{width:100%}
  .save .inline-label select{width:100%}
  .narrow{padding:var(--s7) 0 var(--s9)}
  .dochead{flex-direction:column;gap:var(--s4)}
  footer.foot{padding:var(--s7) 0 var(--s8)}
}

@media (max-width: 400px) {
  .wrap{padding:0 var(--s4)}
  /* Stays 2x2. One column made four full-width cards, so ~460px of scrolling stood
     between the contractor and the first thing they can act on. At 390px each card
     still gets 173px, which fits "$12,480.00" at the reduced size below. Shrink the
     figure, not the layout. */
  .stat{padding:var(--s3) var(--s4)}
  .stat b{font-size:1.25rem}
  .stat .kicker{font-size:0.6875rem}
  .stat small{font-size:0.6875rem}
  .price{font-size:2.05rem}
}

/* Any pointer-coarse device: touch targets of at least 44px. */
@media (pointer: coarse) {
  .btn{min-height:44px}
  /* 44, not 38. `.btn.small` is the Add button on every search result and both
     dashboard primaries — the most-tapped controls in the app. Shaving 6px off the
     minimum on exactly those is the wrong trade. */
  .btn.small{min-height:44px}
  .bar nav a{min-height:44px;display:flex;align-items:center}
  .link{min-height:44px}
  /* A 13px checkbox is a mis-tap. The box stays visually small but gets a 44px
     touch area via the label wrapper, so the hit region matches the intent. */
  .checkline{min-height:44px}
  /* The .customgrid form-control reset (`.customgrid .checkline input{width:auto}`)
     is the same specificity as a bare `.checkline input[type=checkbox]` and sits
     later in the file, so it wins the tie. Matching its selector and adding the
     attribute puts this one ahead without reaching for !important. */
  .customgrid .checkline input[type="checkbox"]{width:24px;height:24px;
    margin-top:0;flex:0 0 auto}
}

/* A stated fact rather than a one-item dropdown. */
.fixed-field{display:grid;gap:5px;padding:12px 13px;background:var(--bg-sub);
  border:1px solid var(--line);border-radius:var(--r-sm)}
.fixed-label{font:540 var(--t-sm) var(--sans);color:var(--ink)}
.fixed-field strong{font-weight:640}

/* finder: one box for search and scan */
.finder{margin:var(--s5) 0 var(--s7)}
.finder #q{flex:1 1 auto}
#scanwrap{margin-top:var(--s4)}
#cam{width:100%;max-width:440px;border-radius:var(--r);background:#000;
  aspect-ratio:4/3;object-fit:cover;display:block}
#results{margin-top:var(--s4);display:grid;gap:var(--s2)}
.hit{display:flex;align-items:center;gap:var(--s3);padding:var(--s3);
  background:var(--bg-sub);border:1px solid var(--line);border-radius:var(--r-sm)}
.hit .n{flex:1 1 auto;min-width:0;font-size:var(--t-sm)}
.hit .n small{display:block;color:var(--ink-3);font-size:var(--t-xs)}
.hit .p{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap}
.hit .stale{color:var(--flag);font-size:var(--t-xs)}
.qtyform{margin:0}
/* input.qty is defined once, in the stepper block below. */

/* ================================================================ stacked tables
 * Horizontal scroll keeps a table readable but it is a compromise: a phone user
 * has to scroll sideways to see the number they came for. So tables marked
 * .stackable become a list of cards below 640px, each cell carrying its own label
 * from data-label. The markup is unchanged, so the desktop table is untouched and
 * there is no duplicate content for a screen reader to read twice.
 */
/* 820, NOT 640. Above 640 these tables stopped being cards and went back to a real
   table — but `.scroller table.lines{min-width:560px}` then made them wider than the
   column they sit in, so on a tablet the builder scrolled sideways inside its own box
   and the "remove" control ended up ~4px from the screen edge. An iPad in portrait is
   768px and got the worst of both: too narrow for a comfortable table, too wide to be
   given cards.
   Only THIS block moves. The document table (.doc .lines) keeps its own 640 breakpoint
   further down — a quote is a document and should stay one at every width, which is why
   it has the separate unit-column collapse rather than being stackable at all. */
@media (max-width: 820px) {
  table.lines.stackable{border:0;box-shadow:none;background:transparent;
    display:block;width:100%}
  table.lines.stackable tbody{display:block;width:100%}
  /* THE ACTUAL CAUSE of the sideways scroll. `.scroller table.lines` sets
     min-width:560px so the DESKTOP table keeps readable columns inside its horizontal
     scroller. min-width beats max-width:100% and beats the container, so on a phone
     the card list was still laid out 560px wide: the page scrolled 186px sideways,
     and once that was contained the money columns were simply clipped off instead.
     Cards do not need a minimum — they stack. */
  .scroller table.lines.stackable{min-width:0}
  table.lines.stackable thead{position:absolute;width:1px;height:1px;overflow:hidden;
    clip:rect(0 0 0 0)}                        /* headings move into data-label */
  table.lines.stackable tbody tr{display:flex;flex-direction:column;
    background:var(--surface);
    border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--e1);
    padding:var(--s3) var(--s4);margin-bottom:var(--s3)}
  /* [hidden] is display:none in the UA stylesheet at almost no specificity, so the
     rule above BEAT IT and un-hid every collapsed swap-alternatives row — they showed
     as empty cards between the real lines. Any display: on a tr must exempt [hidden]. */
  table.lines.stackable tbody tr[hidden]{display:none}
  /* tr.flagged is (0,1,1) and loses to the card rule above at (0,2,3), so a
     needs-review line looked IDENTICAL to a clean one on a phone — the amber was the
     only at-a-glance signal that a price or a match wants checking, and a phone is
     where these quotes actually get built. Re-asserted at matching specificity, with
     an inset bar so it also survives for anyone who cannot pick out the wash. */
  table.lines.stackable tbody tr.flagged{background:var(--flag-wash);
    box-shadow:inset 3px 0 0 var(--flag), var(--e1)}
  table.lines.stackable tbody tr:hover{background:var(--surface)}
  table.lines.stackable tbody td{display:flex;justify-content:space-between;
    align-items:baseline;gap:var(--s4);padding:5px 0;border:0;text-align:left;
    /* min-width:0 is what lets a flex item shrink below its content size. Without it
       a long product name ("5/8" x 4' x 8' (15.5mm) Spruce Plywood Standard
       Sheathing") cannot shrink, so the row is as wide as the name and the page
       scrolls sideways. anywhere breaks inside the run of digits in a sku too. */
    min-width:0;overflow-wrap:break-word}
  /* The Item cell is a name plus a <small> sku. Left as flex siblings they share one
     row, so the sku is squeezed into ~40px and wraps mid-number. Giving it a full-width
     flex line puts it under the name, where it reads as a caption and stays intact. */
  table.lines.stackable tbody td[data-label="Item"]{flex-wrap:wrap;
    align-items:flex-start}
  table.lines.stackable tbody td[data-label="Item"] br{display:none}
  table.lines.stackable tbody td[data-label="Item"] small{flex:1 0 100%;
    margin-top:2px}
  table.lines.stackable tbody td::before{content:attr(data-label);
    font:640 var(--t-xs) var(--sans);color:var(--ink-3);flex:0 0 auto}
  table.lines.stackable tbody td:empty{display:none}
  /* The money cell is what they opened the page for, so it leads. */
  /* "Line" is what the builder calls the line total; Total/Invoiced are the
     dashboard's and the clients page's names for the same idea. Listing only the
     latter two meant the builder promoted nothing and led every card with Qty. */
  table.lines.stackable tbody td[data-label="Line"],
  table.lines.stackable tbody td[data-label="Total"],
  table.lines.stackable tbody td[data-label="Invoiced"]{
    order:-1;font-size:1.15rem;font-weight:700;padding-bottom:var(--s2);
    margin-bottom:var(--s2);border-bottom:1px solid var(--line)}
  table.lines.stackable tfoot{display:block;width:100%;background:var(--surface);
    border:1px solid var(--line);border-radius:var(--r);padding:var(--s3) var(--s4)}
  table.lines.stackable tfoot tr{display:flex;justify-content:space-between}
  table.lines.stackable tfoot th{border:0;padding:3px 0}
  /* The markup ends each footer row with an empty <th> for the actions column. Under
     space-between that empty cell claims the right edge and pushes every amount to
     the middle, so Subtotal, tax and Total do not align with each other. td:empty
     only covers tbody, hence the th rule. */
  table.lines.stackable tfoot th:empty{display:none}
  table.lines.stackable tfoot th:last-child{text-align:right}
  /* Nothing to scroll once the cards fit, and `visible` here is deliberate: it lets
     a focus ring or a dropdown escape the box instead of being cut off. Restored only
     because min-width:0 above means there is no longer any overflow to contain. */
  .scroller:has(table.stackable){overflow-x:visible}
}
.nowrap{white-space:nowrap}
tr.alts td{background:var(--bg-sub);padding:var(--s4)}
tr.alts .hit{background:var(--surface)}

/* ---------------------------------------------------------------- quote builder
 * The finder sticks to the top on a phone because the contractor's thumb is at the
 * bottom and the list below can get long — scrolling back up to add the next item
 * is the single most repeated action in the app.
 */
.builder-head { padding-bottom: var(--s3); }
.builder-head .running { text-align: right; }
.builder-head .running b { display: block; font-size: var(--t-xl); line-height: 1.1; }
.builder-head .running small { display: block; }
/* z-index above the header (40), because at top:0 on a phone the finder otherwise
   slides UNDER the translucent blurred bar and the input is half-legible. */
.sticky-finder { position: sticky; top: var(--s2); z-index: 45; }
.micro { font-size: var(--t-xs); margin-top: var(--s2); }
.row.wrap { flex-wrap: wrap; gap: var(--s3); }

@media (max-width: 640px) {
  .builder-head .running { text-align: left; }
  .sticky-finder { top: 0; border-radius: 0; margin-left: calc(-1 * var(--s4));
                   margin-right: calc(-1 * var(--s4)); }
}

/* ---------------------------------------------------------------- store picker */
.storelist {
  max-height: 320px; overflow-y: auto; border: 1px solid var(--line);
  border-radius: var(--r); margin-bottom: var(--s3); background: var(--bg);
}
.storerow {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3); padding: var(--s3) var(--s3);
  border-bottom: 1px solid var(--line);
}
.storerow:last-child { border-bottom: 0; }
.storepick-home { display: flex; align-items: flex-start; gap: var(--s3);
                  cursor: pointer; flex: 1; margin: 0; }
.storepick-home b { display: block; font-weight: 600; font-size: var(--t-sm); }
.storepick-home small { display: block; font-size: var(--t-xs); }
.storepick-also { display: flex; align-items: center; gap: 6px; margin: 0;
                  font-size: var(--t-xs); color: var(--ink-3); cursor: pointer;
                  white-space: nowrap; }
.storerow input[type="radio"], .storerow input[type="checkbox"] {
  width: auto; margin: 2px 0 0; accent-color: var(--brand);
}

/* ---------------------------------------------------------------- result thumbnails
 * White background regardless of theme: supplier product shots are cut out on white,
 * so on a dark background they show as a bright rectangle unless the tile is white too.
 */
.hit .thumb{width:42px;height:42px;flex:0 0 auto;border-radius:var(--r-sm);
  background:#fff;border:1px solid var(--line);object-fit:contain;padding:2px}
.hit .thumb.empty{background:var(--surface)}

/* ---------------------------------------------------------------- custom line */
.customline{margin-bottom:var(--s5)}
.customline summary{cursor:pointer;font-weight:600;font-size:var(--t-sm);
  color:var(--brand)}
.customline[open] summary{margin-bottom:var(--s4)}
.customgrid{display:grid;gap:var(--s4);
  grid-template-columns:minmax(0,2.2fr) 70px 80px 110px}
.customgrid .checkline{grid-column:1/-1;display:flex;align-items:flex-start;
  gap:var(--s3);font-size:var(--t-sm);font-weight:500}
.customgrid .checkline input{width:auto;margin-top:3px;accent-color:var(--brand)}
.customgrid .checkline small{display:block;font-weight:400}
.customgrid button{grid-column:1/-1;justify-self:start}
@media (max-width:640px){
  .customgrid{grid-template-columns:1fr 1fr}
  .customgrid label:first-child{grid-column:1/-1}
}

/* ---------------------------------------------------------------- order sheet
 * Built to be printed and handed across a counter, so it is deliberately plain and
 * the barcodes are sized to scan off paper.
 */
.ordersheet{max-width:760px;margin:0 auto}
.orderrow{display:flex;align-items:center;gap:var(--s5);padding:var(--s4) 0;
  border-bottom:1px solid var(--line);page-break-inside:avoid}
.orderrow .meta{flex:1 1 auto;min-width:0}
.orderrow .meta b{display:block;font-size:var(--t-sm)}
.orderrow .meta small{color:var(--ink-3);font-size:var(--t-xs)}
.orderrow .want{font:700 var(--t-lg)/1 var(--sans);white-space:nowrap;
  font-variant-numeric:tabular-nums}
.orderrow svg{display:block;background:#fff}
.nocode{font-size:var(--t-xs);color:var(--flag);max-width:180px;text-align:right}
@media print{
  header.bar,.noprint,footer{display:none!important}
  .ordersheet{max-width:none}
  a{text-decoration:none;color:#000}
  body{background:#fff}
}

/* ---------------------------------------------------------------- bottom tab bar
 * Phones only. Primary navigation was a hamburger at the TOP of the screen, which on a
 * phone held one-handed is the furthest reachable point from the thumb — and this app
 * is used standing at a rack with one hand on a cart.
 *
 * env(safe-area-inset-bottom) is not optional: without it the bar sits under the iPhone
 * home indicator and the bottom row of taps lands on the system gesture area instead of
 * the app.
 */
.tabbar{display:none}

@media (max-width: 640px) {
  .tabbar{
    display:flex; position:fixed; left:0; right:0; bottom:0; z-index:60;
    align-items:flex-end; justify-content:space-around;
    padding:6px 4px calc(6px + env(safe-area-inset-bottom, 0px));
    background:color-mix(in srgb, var(--bg) 92%, transparent);
    backdrop-filter:saturate(180%) blur(14px);
    border-top:1px solid var(--line);
  }
  .tabbar .tab{
    flex:1 1 0; min-width:0; min-height:52px;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    gap:3px; padding:4px 2px; border-radius:var(--r-sm);
    color:var(--ink-3); text-decoration:none; font:600 0.6875rem/1 var(--sans);
  }
  .tabbar .tab svg{width:23px; height:23px; fill:currentColor}
  .tabbar .tab.on{color:var(--brand)}
  .tabbar .tab:active{background:var(--bg-sub)}
  .tabbar .tab:hover{text-decoration:none}

  /* The raised centre action, the way a compose button sits in a mail app. */
  .tabbar .tab-new{flex:0 0 auto; margin:0 2px}
  .tabbar .tab-new button{
    width:52px; height:52px; border-radius:50%; border:0;
    background:var(--brand); color:#fff; box-shadow:var(--e2);
    display:flex; align-items:center; justify-content:center; cursor:pointer;
  }
  .tabbar .tab-new button svg{width:26px; height:26px; fill:currentColor}
  .tabbar .tab-new button:active{transform:scale(.94)}

  /* Content must clear the bar. Without this the last line of a quote, or the Save
     button, sits underneath it and cannot be tapped at all. */
  main.wrap{padding-bottom:calc(76px + env(safe-area-inset-bottom, 0px))}
  footer.foot{padding-bottom:calc(84px + env(safe-area-inset-bottom, 0px))}
  /* The sticky finder must stay above the tab bar's blur, not tuck under it. */
  .sticky-finder{z-index:45}
}

/* ---------------------------------------------------------------- app-like feel
 * Small things that separate "web page in a browser" from "app". Each is one line and
 * each is something a native control does for free.
 */
html{
  -webkit-text-size-adjust:100%;      /* iOS silently inflates text without this */
  text-size-adjust:100%;
}
body{
  /* No grey flash box on every tap. :active states below give real feedback instead. */
  -webkit-tap-highlight-color:transparent;
  /* Stops the whole page rubber-banding behind a fixed tab bar in standalone mode. */
  overscroll-behavior-y:contain;
}
@media (pointer: coarse) {
  /* Immediate physical feedback. A tap with no response for 300ms feels broken even
     when the request is fast. */
  .btn:active{transform:scale(.97)}
  .hit .btn:active{transform:scale(.94)}
}

/* CROSS-DOCUMENT VIEW TRANSITIONS ARE DELIBERATELY NOT ENABLED.
 *
 * `@view-transition { navigation: auto; }` animates a full page load like a screen
 * change, which is exactly the app-like touch this section is for. It was tried and
 * removed: in Chromium it leaves the document permanently un-idle, so every automated
 * check that waits for the page to settle hangs — tools/shots.py could not click a
 * button on any page while it was on.
 *
 * That is disqualifying on its own terms. The screenshot harness is how this project
 * verifies mobile layout at all, after several rounds of "looks fine to me, still broken
 * for you", and an animation nobody can regression-test is not worth trading that for.
 * The requirement here was "fast and easy", not "animated".
 *
 * Revisit when the transition can be scoped to specific elements and the idle problem
 * is gone; a decorative flourish must not cost the ability to test everything else.
 */

/* ---------------------------------------------------------------- four-card rows
 * .cols is auto-fit at minmax(258px, 1fr), which on a 1140px container resolves to
 * THREE columns. Sections with four cards therefore orphan the fourth onto a row of its
 * own — visible in "Built to be trusted", the FAQ and "What isn't built yet".
 *
 * A 2x2 for four-card groups reads as deliberate where 3+1 reads as a mistake. Selected
 * with :has() on the count rather than a hand-added class, so the layout follows the
 * content and nobody has to remember to tag the section when a card is added or removed.
 */
@media (min-width: 900px) {
  .cols:has(> :nth-child(4)):not(:has(> :nth-child(5))) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* The header's signup CTA was UNREADABLE: `.bar nav a{color:var(--ink-2)}` is (0,2,1)
 * and beats `.btn{color:#fff}` at (0,1,0), so the most important button on the marketing
 * site rendered slate-grey text on indigo — about 1.3:1 contrast, which reads as a blank
 * purple pill. It looked like a loading state on every signed-out page. */
.bar nav a.btn{color:#fff}
.bar nav a.btn.ghost{color:var(--ink)}

/* The closing band is the only TERMINAL section, so it should not look like the five
   above it. With six sections, strict alternation forces the last one plain — which left
   the final call to action visually identical to "How it works". A brand wash plus a rule
   above it says "this is the end and this is the action" without another grey rectangle. */
section.band.close{
  background:
    radial-gradient(58% 120% at 50% 100%, var(--brand-wash) 0%, transparent 72%),
    var(--bg);
  border-top:1px solid var(--line);
}
section.band.close h2{margin-bottom:var(--s3)}

/* The hero's two CTAs are a PAIR and must read as two buttons of different weight, not as
   a button and a link. --line-strong (#cfd8e3) measured 1.44:1 against the page, well
   under the 3:1 WCAG asks for the boundary of a UI component, so the secondary button had
   no visible edge. --cta-edge is 3.47:1.
   Note: a 1.5px border was tried first and Chromium SNAPPED it to 1px in computed style,
   so the weight has to come from colour, not from a fractional width. */
:root{ --cta-edge:#7d8b9e }
@media (prefers-color-scheme: dark){ :root{ --cta-edge:#5d6b83 } }
:root[data-theme="dark"]{ --cta-edge:#5d6b83 }
:root[data-theme="light"]{ --cta-edge:#7d8b9e }
.hero-cta .btn.ghost{border-color:var(--cta-edge);box-shadow:var(--e1)}
.hero-cta .btn.ghost:hover{border-color:var(--ink-2)}

/* ---------------------------------------------------------------- the dimension line
 * The signature device of the drawing-sheet direction, and the reason it beat the other
 * two: this reader measures for a living, so the strongest possible statement of "ninety
 * seconds" is not a gradient on the words — it is a DIMENSION drawn under them, with a
 * witness tick at each end, exactly as he would mark it on a board.
 *
 * "Quotebook" is itself the drafting term for the ruled border of a drawing, so the page
 * is finally named after what it looks like.
 *
 * Built on the existing --brand and --s scale; the ticks reuse the registration-mark
 * motif already in the logo mark. The rule spans the phrase, not the line box, because
 * .grad is inline and the span is what is being measured.
 */
.hero h1 .grad{position:relative;padding-bottom:0.16em}
.hero h1 .grad::after{
  content:"";position:absolute;left:0;right:0;bottom:0;height:3px;
  background:currentColor;border-radius:2px;opacity:.95;
}
/* Witness ticks: the short verticals that close a dimension at each end. */
.hero h1 .grad::before{
  content:"";position:absolute;left:0;right:0;bottom:0;height:11px;
  border-left:3px solid currentColor;border-right:3px solid currentColor;
  border-radius:1px;opacity:.95;
}
@media (max-width:480px){
  .hero h1 .grad::after{height:2.5px}
  .hero h1 .grad::before{height:9px;border-left-width:2.5px;border-right-width:2.5px}
}
/* A gradient FILL plus a gradient RULE is two competing emphases on one phrase. The rule
   is the stronger idea, so the fill becomes a flat brand colour and the measurement does
   the work. */
.hero h1 .grad{background:none;-webkit-text-fill-color:currentColor;color:var(--brand)}

/* A result already added. The search results now stay on screen after an add, so this is
   the only thing telling the contractor which ones they have taken — without it they
   re-add the same stud four times. */
.hit .btn.added{background:var(--ok);border-color:var(--ok);color:#04231a;opacity:1}

/* The builder's two states. Both sets of markup are always in the DOM so a fragment can
   patch a row in without JavaScript having to construct a table; this decides which one
   the contractor sees. */
#builder[data-empty] .scroller,
#builder[data-empty] .save,
#builder[data-empty] .micro{display:none}
#builder:not([data-empty]) .empty{display:none}

/* ---------------------------------------------------------------- quantity steppers
 * Changing 3 sheets to 4 is the most repeated action on this screen, and doing it through
 * a text field means opening the keyboard every time.
 *
 * The buttons are only shown once patch.js has marked the form (data-js). A stepper that
 * silently does nothing is worse than no stepper, and the bare field plus its form works
 * without JavaScript.
 */
.qtyform{margin:0;display:flex;align-items:center;gap:2px;justify-content:flex-end}
.qtyform .step{display:none}
.qtyform[data-js] .step{
  display:flex;align-items:center;justify-content:center;
  width:34px;height:34px;flex:0 0 auto;
  border:1px solid var(--line-strong);border-radius:var(--r-sm);
  background:var(--surface);color:var(--ink);
  font:600 17px/1 var(--sans);cursor:pointer;user-select:none;
}
.qtyform[data-js] .step:hover{border-color:var(--ink-3);background:var(--bg-sub)}
.qtyform[data-js] .step:active{transform:scale(.94)}
.qtyform[data-js] .step:disabled{opacity:.4;cursor:default;transform:none}
input.qty{
  width:3.4em;padding:5px 7px;text-align:right;font-variant-numeric:tabular-nums;
  /* 16px minimum: iOS Safari ZOOMS the whole page when a focused input is smaller, and
     the page never zooms back out. */
  font-size:16px;
}
@media (pointer: coarse){
  /* 44px on touch, matching every other control here. */
  .qtyform[data-js] .step{width:44px;height:44px;font-size:20px}
  input.qty{width:3.1em;padding:8px 6px}
}
@media (max-width: 640px){
  /* In the stacked cards the Qty row has the whole width, so the steppers can breathe. */
  .qtyform{justify-content:flex-end;gap:var(--s2)}
}

/* The empty-state action pair. At 320px the two buttons total ~310px, so side by side
   they sit 2px from each bezel — measured, not eyeballed. Stacking them full-width below
   the SE's width is both reachable and unambiguous; there is no reason for two equally
   sized boxes to fight for one line on the narrowest phone we support. */
.empty .row{flex-wrap:wrap;justify-content:center;gap:var(--s3)}
@media (max-width: 380px){
  .empty .row{flex-direction:column;align-items:stretch}
  .empty .row .btn,
  .empty .row form{width:100%}
  .empty .row form .btn{width:100%}
}

/* ---------------------------------------------------------------- invoicing
 *
 * The document action bar, the payment form and the payment list. All of it sits on the
 * quote/invoice page, which is ALSO the page the PDF is printed from — so everything
 * interactive here carries .noprint, and print.css must not have to know these class
 * names exist.
 */
.pill.dim{background:var(--raised);color:var(--ink-3)}

/* Overdue is the one thing on the money screens that should stop a contractor's eye.
   Colour alone is not enough — the cell also says the word "overdue", because the most
   common colour-blindness makes amber-on-grey almost invisible. */
.overdue{color:var(--flag);font-weight:600}

.docbar{display:flex;justify-content:space-between;align-items:center;gap:var(--s4);
  flex-wrap:wrap;margin-bottom:var(--s4)}
.docbar-left{display:flex;gap:var(--s4);align-items:center;flex-wrap:wrap;
  font-size:var(--t-sm)}
.docbar-right{display:flex;gap:var(--s2);align-items:center;flex-wrap:wrap;
  margin-left:auto}
/* The forms wrap single buttons; without this they take a full line and the bar
   collapses into a column. display:contents lets the button sit in the flex row. */
.docbar-right form{display:contents}

.payform{margin-top:var(--s6)}
.payform h3{margin:0 0 var(--s4)}
/* auto-fit rather than fixed columns: on a phone this is one column with a full-width
   Record button, on a laptop it is a single row. No breakpoint to keep in sync. */
.payfields{display:grid;gap:var(--s3);
  grid-template-columns:repeat(auto-fit,minmax(148px,1fr));align-items:end}
.payfields label{display:flex;flex-direction:column;gap:6px;font-size:var(--t-xs);
  color:var(--ink-3)}
.payfields input,.payfields select{width:100%}
.payfields .btn{white-space:nowrap}

.paylist{width:100%;border-collapse:collapse;margin-top:var(--s5);font-size:var(--t-sm)}
.paylist caption{text-align:left;padding-bottom:var(--s2);font-size:var(--t-xs);
  text-transform:uppercase;letter-spacing:.08em}
.paylist td{padding:6px 0;border-top:1px solid var(--line)}
/* Without this the amount and the remove control butt straight together and
   read as one token: "$1,000.00remove". */
.paylist td + td{padding-left:var(--s4)}
.paylist td.num{text-align:right}
.paylist form{display:inline}
.link.danger{color:var(--flag)}

@media (max-width:640px){
  /* A 44px target: this is the button a contractor taps standing in a driveway. */
  .payfields .btn{grid-column:1/-1;min-height:44px}
  .docbar-right{width:100%}
  .docbar-right .btn{flex:1;text-align:center}
}

/* The unit price is shown twice in the markup: as its own column for wide screens and
   as a caption under the description for phones. Exactly one is ever visible. */
.unitline{display:none}

@media (max-width: 640px){
  /* THE DOCUMENT TABLE ON A PHONE.
   *
   * A quote or invoice is not turned into cards the way the builder's table is — it has
   * to keep looking like a document, because it is the thing the customer receives. But
   * four columns need 560px, and `.scroller table.lines{min-width:560px}` meant that at
   * 390px the table scrolled sideways INSIDE its container and the whole right-hand
   * money column sat off-screen. Balance due — the one number an invoice exists to
   * communicate — was invisible, and because the PAGE did not overflow, nothing in the
   * screenshot harness noticed.
   *
   * Dropping the Unit column to a caption under the description leaves Qty, Description
   * and Amount, which fit with room to spare. Hiding every cell in that column collapses
   * it to zero width, so the tfoot's colspan=3 still lands its figure in the last
   * column — no markup change needed for the totals rows. */
  .doc .scroller table.lines{min-width:0}
  .doc .lines thead th:nth-child(3),
  .doc .lines tbody td:nth-child(3),
  .doc .lines tfoot th.unitcol{display:none}
  .doc .lines .unitline{display:block;margin-top:2px}
  .doc .lines td,.doc .lines th{padding-left:var(--s2);padding-right:var(--s2)}

  /* When a row carries an Owing figure that is what the page was opened to find out, so
     it leads the card and the invoice Total steps back to an ordinary line. Without this
     both would be promoted and the card would open with two bold numbers. */
  table.lines.stackable tbody tr:has(td[data-label="Owing"]) td[data-label="Total"]{
    order:0;font-size:inherit;font-weight:inherit;
    padding-bottom:5px;margin-bottom:0;border-bottom:0}
  table.lines.stackable tbody td[data-label="Owing"]{
    order:-1;font-size:1.15rem;font-weight:700;padding-bottom:var(--s2);
    margin-bottom:var(--s2);border-bottom:1px solid var(--line)}
}

/* Print is never the phone layout: the PDF is rendered at Letter width, so the media
   query above cannot match. Asserted anyway — a printed invoice that lost its Unit
   column because someone printed from a phone would be wrong on paper, and paper is
   what the customer keeps. */
@media print{
  .doc .lines thead th:nth-child(3),
  .doc .lines tbody td:nth-child(3),
  .doc .lines tfoot th.unitcol{display:table-cell}
  .doc .lines .unitline{display:none}
}

/* ---------------------------------------------------------------- naming
 *
 * A quote or draft can be given a job name ("Whelan kitchen reno"). The input is styled
 * to read as a heading until it is focused, so the page still looks like a document
 * rather than a form — but it is a real <input> in a real <form>, so Enter saves it with
 * no JavaScript involved. The Save button is revealed on focus rather than hidden behind
 * a script, which keeps the whole feature working with JS off and offline.
 */
.renameform{display:flex;align-items:center;gap:var(--s2);margin:0 0 var(--s4);
  max-width:100%}
.renametitle{font:700 var(--t-lg) var(--sans);letter-spacing:-.02em;color:var(--ink);
  background:transparent;border:1px solid transparent;border-radius:var(--r);
  padding:4px 8px;margin:0;width:100%;max-width:34ch}
.renametitle::placeholder{color:var(--ink-3);font-weight:500}
.renametitle:hover{border-color:var(--line)}
.renametitle:focus{background:var(--surface);border-color:var(--brand-ring);outline:none}
/* Hidden by VISIBILITY, not display: the button keeps its space, so revealing it on
   focus does not shove the title sideways under the cursor mid-edit. It is still
   reachable by Tab, which is how a keyboard user finds it. */
.renamesave{visibility:hidden;flex:0 0 auto}
.renameform:focus-within .renamesave{visibility:visible}

/* The job name ON the printed document, under the number. Quiet: the number is the
   identity and must stay the most prominent thing in that corner. */
.doctitle{font:600 var(--t-sm) var(--sans);color:var(--ink-2);margin-top:2px;
  max-width:32ch;overflow-wrap:break-word}

@media (max-width:640px){
  /* No hover on a touch screen, so the affordance has to be permanent or it does not
     exist. Both the border and the button are always shown. */
  .renametitle{border-color:var(--line);max-width:none}
  .renamesave{visibility:visible}
}

/* ---------------------------------------------------------------- kent browse
 *
 * The standalone catalogue. No JavaScript is involved: the search is a GET form, the
 * facets are links, the Add buttons are POSTs. So every state this page can be in is
 * reachable by URL and reproducible in a screenshot.
 */
.browsebar{display:flex;gap:var(--s3);align-items:center;flex-wrap:wrap;
  margin-bottom:var(--s4)}
/* width:auto is load-bearing. The base rule at the top of this file gives every input
   width:100%, which beats a flex-basis — so the search field claimed the whole row and
   pushed the sort and the button onto two more lines. */
.bigsearch{flex:1 1 260px;width:auto;min-width:0;font-size:var(--t-md)}
.browsebar select{flex:0 1 auto;width:auto;min-width:9rem}

.targetbar{display:flex;gap:var(--s3);align-items:center;flex-wrap:wrap;
  margin-bottom:var(--s5);padding:var(--s3) var(--s4)}
.targetbar label{font:640 var(--t-xs) var(--sans);color:var(--ink-3);flex:0 0 auto}
.targetbar select{flex:1 1 200px;width:auto;min-width:0}

.browse{display:grid;gap:var(--s5);grid-template-columns:220px 1fr;align-items:start}
.filters .facet{margin-bottom:var(--s5)}
.filters h3{font:640 var(--t-xs) var(--sans);text-transform:uppercase;
  letter-spacing:.08em;color:var(--ink-3);margin:0 0 var(--s2)}
.filters ul{list-style:none;margin:0;padding:0}
.filters li{margin:0 0 4px}
.filters li a{font-size:var(--t-sm);color:var(--ink-2);text-decoration:none}
.filters li a:hover{color:var(--brand)}
.chosen{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-bottom:var(--s5)}
/* .pill is white-space:nowrap, which is right for a status but wrong for a Kent category
   ("Building Materials, Lumber & Sheet Goods") — unbroken it runs straight out of the
   column. Chips are the one pill that must wrap. */
.pill.chip{white-space:normal;overflow-wrap:anywhere;max-width:100%;
  text-decoration:none;cursor:pointer}

.count{margin:0 0 var(--s3);font-size:var(--t-sm)}
.grid{display:grid;gap:var(--s4);
  grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}
.pcard{display:flex;flex-direction:column;background:var(--surface);
  border:1px solid var(--line);border-radius:var(--r-lg);overflow:hidden;
  box-shadow:var(--e1)}
/* A FIXED BOX with a reserved aspect ratio. Kent's images arrive one at a time over the
   network; without this the whole grid reflows under the cursor as each lands, and an
   Add button moves out from under a finger mid-tap. */
/* A FIXED height, not a square. Square thumbnails pushed all but four products below
   the fold on a laptop. The height is fixed rather than intrinsic so the grid does not
   reflow as images arrive over the network — an Add button that moves out from under a
   finger mid-tap is worse than a slightly small picture. */
.pthumb{width:100%;height:132px;object-fit:contain;background:#fff;
  border-bottom:1px solid var(--line);display:block}
.pthumb.empty{background:var(--raised)}
@media (max-width:640px){ .pthumb{height:104px} }
.pbody{display:flex;flex-direction:column;gap:6px;padding:var(--s3) var(--s4) var(--s4);
  flex:1}
.pname{font:600 var(--t-sm) var(--sans);color:var(--ink);overflow-wrap:anywhere}
.pmeta{font-size:var(--t-xs);overflow-wrap:anywhere}
.oos{color:var(--flag)}
/* Stacked, not a row: at 320px a price, a cost caption and a 44px button cannot share
   one line, and the money is what must never be squeezed. */
.pfoot{display:grid;gap:var(--s2);margin-top:auto;padding-top:var(--s2)}
.prices{display:flex;flex-direction:column}
.prices .strong{font:700 var(--t-md) var(--sans)}
.prices small{font-size:var(--t-xs)}
.addform .btn{width:100%;min-height:40px}
.kentlink{font-size:var(--t-xs)}

.pager{display:flex;gap:var(--s3);align-items:center;justify-content:center;
  margin:var(--s6) 0 var(--s10)}

@media (max-width:860px){
  /* The facet sidebar becomes a band ABOVE the grid. A 220px column at 390px would
     leave the products 130px wide; a collapsed <details> would hide the filters behind
     a tap on the screen where narrowing 3,000 results matters most. */
  .browse{grid-template-columns:1fr}
  .filters{display:flex;flex-wrap:wrap;gap:var(--s4)}
  .filters .facet{margin-bottom:0;flex:1 1 150px}
  .filters ul{max-height:132px;overflow:auto}
  .chosen{flex-basis:100%}
}
@media (max-width:640px){
  .grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:var(--s3)}
  .addform .btn{min-height:44px}
  .browsebar select,.browsebar .btn{flex:1 1 auto}
}

/* ---------------------------------------------------------------- plans */
.plans{display:grid;gap:var(--s4);grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
  align-items:start;margin-bottom:var(--s6)}
.plan{display:flex;flex-direction:column;gap:var(--s3)}
.plan.current{border-color:var(--brand-ring);box-shadow:0 0 0 1px var(--brand-ring)}
.plan h3{margin:0}
.planprice{margin:0}
.planprice b{font:700 1.7rem var(--sans);letter-spacing:-.03em}
.planfeatures{list-style:none;margin:0;padding:0;font-size:var(--t-sm);flex:1}
.planfeatures li{padding:5px 0 5px 22px;position:relative;color:var(--ink-2)}
/* The tick is DRAWN, not typed. A "✓" character depends on the platform font having it
   and on that glyph's metrics; on this machine it rendered as something that was not
   recognisably a tick. Two borders rotated 45 degrees look the same everywhere. */
.planfeatures li::before{content:"";position:absolute;left:4px;top:9px;
  width:5px;height:9px;border:solid var(--ok);border-width:0 2px 2px 0;
  transform:rotate(45deg)}
/* Struck through AND greyed AND a different mark: on the one screen where someone is
   deciding what to pay for, "not included" must not rely on colour alone. */
.planfeatures li.no{color:var(--ink-3);text-decoration:line-through}
.planfeatures li.no::before{left:2px;top:14px;width:10px;height:0;
  border-width:0 0 2px 0;border-color:var(--ink-3);transform:none}
.plan .btn{width:100%;min-height:44px}
.plan .btn[disabled]{opacity:.5;cursor:not-allowed}
.plansnote{max-width:60ch;margin:0 0 var(--s10);font-size:var(--t-sm)}
.notice{margin-bottom:var(--s5);border-color:var(--flag-ring,var(--line))}
.notice h3{margin:0 0 var(--s2)}

/* Disclosure for the client edit form. <details> is the entire mechanism — native, works
   with JavaScript off, and nothing to precache. */
.editclient{margin:0 0 var(--s5)}
.editclient summary{cursor:pointer;font:640 var(--t-sm) var(--sans);color:var(--brand);
  display:inline-block;padding:6px 0;min-height:32px}
.editclient[open] summary{margin-bottom:var(--s3)}
.editclient .card{max-width:32rem}

/* Net terms inline with the Create-invoice button: the decision is made at that moment,
   not on a settings page nobody visits before their first invoice. */
.inline-terms{display:flex;align-items:center;gap:6px}
.terms{display:flex;align-items:center;gap:4px;font:600 var(--t-xs) var(--sans);
  color:var(--ink-3);white-space:nowrap}
.terms input{width:4.5rem;padding:6px 8px;text-align:right}
/* An OUTLINE button, not a filled one. The base .btn is a solid indigo fill, so setting
   only `color` gave red text on indigo — illegible, on the one control where a mis-tap
   is destructive. Destructive actions should also never be the loudest thing on screen:
   this reads as available, not as the thing to press. */
.btn.danger{background:transparent;color:var(--flag);
  border-color:color-mix(in srgb,var(--flag) 42%,transparent)}
.btn.danger:hover{background:var(--flag-wash);border-color:var(--flag)}
.dangerzone{margin-top:var(--s4);padding-top:var(--s4);border-top:1px solid var(--line);
  display:flex;flex-direction:column;gap:6px;align-items:flex-start;max-width:32rem}

/* ---------------------------------------------------------------- editing a quote */
.editbar{display:flex;align-items:center;gap:var(--s3);flex-wrap:wrap;
  margin-bottom:var(--s4);padding:var(--s3) var(--s4)}
.editbar .spacer{flex:1}
.editrow input{width:100%;padding:6px 8px;font-size:var(--t-sm)}
.editrow .qty{max-width:5rem;text-align:right}
.editrow .unitfield{max-width:5rem;margin-top:4px}
.editrow .pricefield{max-width:8rem;text-align:right}
.editrow .rm{display:inline-flex;align-items:center;gap:5px;font-size:var(--t-xs);
  color:var(--ink-3);white-space:nowrap}
.editrow .rm input{width:auto}
@media (max-width:640px){
  /* The editor is a form, not a document, so it stacks like every other form here. */
  .doc .lines .editrow{display:block;padding:var(--s3) 0;
    border-bottom:1px solid var(--line)}
  .doc .lines .editrow td{display:block;padding:3px 0;text-align:left}
  .editrow .qty,.editrow .unitfield,.editrow .pricefield{max-width:none}
}
.setprice{display:flex;gap:4px;align-items:center;justify-content:flex-end}
.setprice input{width:6rem;padding:5px 7px;text-align:right;font-size:var(--t-sm)}
.setprice .btn{padding:5px 9px}
@media (max-width:640px){ .setprice{justify-content:flex-start} }

/* ---------------------------------------------------------------- sharing */
.sharebar{display:flex;align-items:center;gap:var(--s3);flex-wrap:wrap;
  margin-bottom:var(--s4);font-size:var(--t-sm)}
.sharebar label{font:640 var(--t-xs) var(--sans);color:var(--ink-3)}
.sharebar input{flex:1 1 22rem;width:auto;min-width:0;font-size:var(--t-xs);
  font-family:var(--mono,ui-monospace,monospace);padding:7px 9px}
.letterhead{font-size:var(--t-xs);line-height:1.5;margin-top:6px}
.letterhead a{color:inherit}
.docnotes{margin:var(--s5) 0 0;font-size:var(--t-sm);color:var(--ink-2);
  white-space:pre-wrap;max-width:60ch}

/* Store picker tap targets. These rules could not be checked until the province filter
   was fixed — the picker had always rendered EMPTY, so the harness had nothing to
   measure and reported the page clean. Picking a store is done standing in a yard on a
   phone, and picking the wrong one silently changes every price the contractor is
   quoted. */
.storepick-home{min-height:44px;align-items:center;padding:2px 0}
.storerow .storepick-also{display:inline-flex;align-items:center;gap:6px;
  min-height:44px;padding:0 4px;cursor:pointer;white-space:nowrap}
.storerow input[type=radio],.storerow input[type=checkbox]{
  width:20px;height:20px;flex:0 0 auto;margin:0}

/* ================================================================== print, corrected
 *
 * TWO BUGS THAT REACHED THE CUSTOMER'S PIECE OF PAPER.
 *
 * 1. The setup banner printed on the invoice. `.flash`/`.warn` were never hidden, so an
 *    internal nag — "Set your home store and markup before you quote" — appeared on the
 *    quote a contractor sent, and on the PDF, which pdf.py renders through print media.
 *    Nothing catches this in review because nobody prints while there is a banner up.
 *
 * 2. Printing from dark mode carried the dark palette onto white paper. The tokens are
 *    resolved at render time, so `--ink-3` printed as a pale grey on white and the flag
 *    colour as #fbbf5c — 1.73:1 on paper. The tokens are reset here rather than each
 *    rule being restated, so anything added later prints correctly by default.
 */
@media print{
  :root, :root[data-theme="dark"], :root[data-theme="light"]{
    --bg:#fff; --bg-sub:#fff; --surface:#fff; --raised:#fff;
    --line:#d4dae2; --line-strong:#9aa5b4;
    --ink:#000; --ink-2:#333; --ink-3:#555;
    --brand:#4f46e5; --ok:#047857; --flag:#b45309;
    --ok-wash:#e7f6f0; --flag-wash:#fef4e6; --brand-wash:#eef0fe;
    --e1:none; --e2:none; --e3:none; --e-brand:none;
  }
  /* App chrome is not part of the document. */
  .flash, .warn, .tabbar, .sharebar, .docbar, .editbar{display:none!important}
  .doc{box-shadow:none;border:0;padding:0;margin:0}
  .doc .lines thead th{background:transparent}
  /* A quote must not split a line, or a total, across a page break. */
  table.lines tr, .orderrow, tfoot{break-inside:avoid}
}

/* ================================================================== the finder, capped
 *
 * THE WORST BUG ON THE APP'S MAIN SCREEN. Measured at 390x844 with eight results for
 * "2x4": the finder was 1129px tall inside an 844px viewport, position:sticky at top:0,
 * so once you scrolled past it, it pinned and covered the entire screen. The quote you
 * were building was underneath it and there was no way to get back to it except
 * clearing the search.
 *
 * It also sat at z-index 45 against the header's 40, so it covered the nav on the way.
 *
 * Two fixes: the header outranks page furniture, and the finder is bounded to a fraction
 * of the viewport with the RESULTS scrolling inside it — so the search box and the quote
 * lines are both always reachable. svh, not vh: on iOS Safari vh is the tallest
 * possible viewport, which would put the bottom of the finder under the browser chrome.
 */
header.bar{z-index:50}
.sticky-finder{top:calc(65px + var(--s2))}          /* header is 65px incl. its border */
@media (max-width:760px){
  .sticky-finder{top:calc(59px + var(--s2))}        /* 58px + border below 760 */
}
@media (max-width:640px){
  .sticky-finder{
    top:59px; max-height:calc(62svh - 59px);
    display:flex; flex-direction:column; overflow:hidden;
  }
  /* The search box and the scanner keep their height; only the results give way. */
  .sticky-finder > .row,
  .sticky-finder > #scanwrap{flex:0 0 auto}
  .sticky-finder > #results{
    flex:1 1 auto; overflow-y:auto;
    /* Contain the scroll so flicking the results does not drag the page behind them. */
    overscroll-behavior:contain; -webkit-overflow-scrolling:touch;
  }
}

/* ============================================== the nav collapses sooner than 760px
 *
 * HORIZONTAL OVERFLOW ON EVERY TABLET. The signed-in nav has grown to eleven items —
 * Dashboard, Estimate, Drafts, Quotes, Kent, Invoices, Clients, Settings, Plan, the org
 * name and Sign out — and it does not fit until about 903px. The burger only appeared at
 * 760 and below, so from 761 to 902 the nav simply pushed the page wide: measured
 * scrollWidth 903 against clientWidth 768, the whole document scrolling sideways.
 *
 * tools/shots.py runs 320, 390, 430 and 1280 and stepped straight over the band. An iPad
 * in portrait is 768.
 *
 * These rules come after the ≤760 block, so at equal specificity they win where they
 * overlap. The bar height differs between the two ranges, hence the variable rather than
 * a repeated magic number.
 */
:root{--bar-h:65px}
@media (max-width:760px){ :root{--bar-h:59px} }

/* 920, not 900: measured, the eleven-item nav needs 903px, so 900 still overflowed by
   2px at 901. The extra margin is deliberate headroom — this nav has grown three times
   already and the next item should not reintroduce a sideways-scrolling tablet. */
@media (max-width:920px){
  .burger{display:grid;gap:4px;margin-left:auto;padding:9px;cursor:pointer;
    border:1px solid var(--line);border-radius:var(--r-sm)}
  .burger span{display:block;width:17px;height:1.5px;background:var(--ink-2);
    border-radius:2px}
  .burger:hover{background:var(--raised)}
  .bar nav{position:absolute;top:var(--bar-h);left:0;right:0;margin:0;
    flex-direction:column;align-items:stretch;gap:0;
    background:var(--surface);border-bottom:1px solid var(--line);
    box-shadow:var(--e2);display:none;padding:var(--s2) var(--s4) var(--s4);
    z-index:1}
  .navtoggle:checked ~ nav{display:flex}
  .bar nav a,.bar nav form,.bar nav .who{padding:11px 2px;
    border-bottom:1px solid var(--line);font-size:0.9375rem}
  .bar nav a:last-of-type{border-bottom:0}
  .bar nav .btn{margin-top:var(--s3);width:100%}
}

/* ================================================================== contrast fixes
 *
 * All measured against the background actually painted behind the text, not against the
 * page background — which is how they survived earlier passes.
 *
 * 1. LIGHT --ink-3 was #8494aa: 3.09:1 on white, 2.95:1 on --bg-sub. It carries the Cost
 *    column on the builder, the tab-bar labels, "your cost" on every Kent card and eight
 *    other places. #64748b clears 4.5:1 on both while staying a third step below --ink-2
 *    rather than collapsing the scale. Dark's --ink-3 already measures 4.78–6.45 and is
 *    left alone.
 */
 * A BARE :root IS NOT "LIGHT MODE". Written as `:root, :root[data-theme="light"]` this
 * sat AFTER the prefers-color-scheme:dark block at equal specificity, so it overwrote
 * dark's --ink-3 too — replacing a value that measured 4.78-6.45 with one that measures
 * 3.11 on a dark surface. It made every muted caption in dark mode worse while fixing
 * light. All four selectors are needed, in this order, exactly as --brand-fill below.
 */
:root{ --ink-3:#64748b }                                    /* light: 4.76 on white */
@media (prefers-color-scheme: dark){ :root{ --ink-3:#8494aa } }   /* dark was fine */
:root[data-theme="dark"]{ --ink-3:#8494aa }
:root[data-theme="light"]{ --ink-3:#64748b }
/*

/* 2. A WHITE LABEL ON THE DARK BUTTON was 3.69:1 — and hovering made it WORSE, 2.89:1,
 *    because --brand-hover lightens in dark mode. So the fix has to cover both states or
 *    it moves the failure rather than removing it.
 *
 *    A separate token from --brand: the button needs a DARKER fill to carry white text,
 *    while links and accents on dark surfaces need a LIGHTER tone (see --brand-text
 *    below). One value cannot do both, which is why the original single token failed at
 *    one end or the other whichever way it was moved.
 */
:root{ --brand-fill:var(--brand); --brand-fill-hover:var(--brand-hover) }
@media (prefers-color-scheme: dark){
  :root{ --brand-fill:#5b52ea; --brand-fill-hover:#5348e8 }   /* 5.47 / 6.07 on white */
}
:root[data-theme="dark"]{ --brand-fill:#5b52ea; --brand-fill-hover:#5348e8 }
:root[data-theme="light"]{ --brand-fill:var(--brand); --brand-fill-hover:var(--brand-hover) }
.btn{background:var(--brand-fill)}
.btn:hover{background:var(--brand-fill-hover)}
.tabbar .tab-new button{background:var(--brand-fill)}

/* 3. BRAND TEXT ON DARK SURFACES. --brand (#7c74f7) measures 4.01:1 on --surface and
 *    3.40:1 on --raised, so every link inside a card failed. --brand-2 is already in the
 *    palette and clears 5.37:1 at worst, so this is a re-use, not a new colour.
 */
:root{ --brand-text:var(--brand) }
@media (prefers-color-scheme: dark){ :root{ --brand-text:var(--brand-2) } }
:root[data-theme="dark"]{ --brand-text:var(--brand-2) }
:root[data-theme="light"]{ --brand-text:var(--brand) }
a{color:var(--brand-text)}
.link{color:var(--brand-text)}
.flash{color:var(--brand-text)}

/* KNOWN, NOT FIXED: between 641 and ~820px the builder's "remove" control sits about 4px
   from the viewport edge. Padding the last cell does nothing about it — above 640 the
   table stops being stackable cards and `.scroller table.lines{min-width:560px}` makes it
   wider than its container, so it scrolls horizontally and the button's position on
   screen is set by that scroll rather than by the cell. The real fix is to raise the
   stackable breakpoint from 640 to ~820 so tablets get cards too. That is a deliberate
   layout change needing its own screenshot pass, not a one-liner. tools/shots.py reports
   it on every run so it cannot be forgotten. */

/* ================================================================== form errors
 *
 * A FAILED SIGNUP LOOKED LIKE GOOD NEWS. Errors went through the same page-wide .flash
 * strip as "Quote Q-2026-001 created." — same brand colour, same wash, same full-width
 * band above a 432px form column. --stop and --stop-wash were defined in all three theme
 * blocks and referenced by nothing.
 *
 * This sits WITH the form, in the stop colour, so the thing that went wrong is next to
 * the thing that has to change.
 */
.formerror{
  background:var(--stop-wash); color:var(--stop);
  border:1px solid color-mix(in srgb, var(--stop) 30%, transparent);
  border-radius:var(--r); padding:var(--s3) var(--s4);
  margin:0 0 var(--s5); font-size:var(--t-sm); font-weight:560;
}
/* The auth heading was the browser's 2em default — 31px at every width from 320 to 1600,
   the one heading in the product not on the type scale — and it sat flush against the
   first field. */
.narrow > h1{font-size:var(--t-h1);margin-bottom:var(--s6)}
.narrow form + p{margin-top:var(--s5)}

/* ================================================================== focus & reach */

/* A VISIBLE FOCUS RING EVERYWHERE. Keyboard users had nothing reliable to follow, and
   several controls relied on the browser default that other rules had cancelled. */
:focus-visible{outline:3px solid var(--brand-text);outline-offset:2px;
  border-radius:var(--r-sm)}
input:focus-visible,select:focus-visible,textarea:focus-visible{
  outline:2px solid var(--brand-text);outline-offset:1px}
/* .renametitle set outline:none to look like a heading, which cancelled the ring. It can
   look like a heading at rest and still show focus. */
.renametitle:focus{outline:2px solid var(--brand-text);outline-offset:1px}

/* Skip link. Eleven nav items stood between the top of every page and its content. */
.skip{position:absolute;left:var(--s4);top:-60px;z-index:100;
  background:var(--surface);color:var(--ink);
  border:1px solid var(--line-strong);border-radius:var(--r-sm);
  padding:10px var(--s5);box-shadow:var(--e2);font:600 var(--t-sm) var(--sans);
  text-decoration:none}
.skip:focus{top:var(--s2)}

/* A DISABLED BUTTON THAT LOOKS LIKE A LOADING STATE. The plan buttons were the brand
   fill at opacity .5 — white on indigo composited to 2.31:1, which reads as "this page
   has not finished loading", not "you cannot press this yet". */
.btn:disabled,.btn[disabled]{
  opacity:1;background:var(--raised);color:var(--ink-3);
  border-color:var(--line);box-shadow:none;cursor:not-allowed;transform:none}
.btn:disabled:hover,.btn[disabled]:hover{background:var(--raised)}

/* A <summary> is the whole tap target of a disclosure, and these were 22px and 32px
   tall on a touch screen. */
@media (pointer: coarse){
  .customline summary,.editclient summary{min-height:44px;display:flex;align-items:center}
}

/* ================================================================== plan cards
 *
 * Two defects, both measured at every width.
 *
 * 1. THREE CARDS RENDERED 2+1 from 544 to 799px — two side by side and the third alone
 *    beside an empty half. minmax(240px,1fr) fits three only from 800 up.
 * 2. A 52px HOLE between the price and the feature list, at every width, from a flex gap
 *    plus two margins that do not collapse. Two rules meant to equalise card height
 *    (align-items:stretch and an auto-margin CTA) were being overridden elsewhere and
 *    had no effect at all, so the CTAs sat at four different heights.
 */
.plans{align-items:stretch}
.plan{display:flex;flex-direction:column;gap:var(--s3)}
.plan .planprice,.plan .price{margin-bottom:0}
.plan ul,.plan .planfeatures{margin-top:0}
/* Pushes the CTA to the bottom of whichever card is tallest, so the buttons line up. */
.plan form,.plan > .btn{margin-top:auto}
@media (max-width:799px){
  /* Below three-across, go to one. There is no orphan-free two-column layout for three
     items, and a full-width card reads better than a stranded one. */
  .plans{grid-template-columns:1fr}
}

/* The swap/remove separator belongs to the inline table layout. In card mode the row is
   space-between, so the dot detaches from both controls and hangs in the middle. */
@media (max-width:820px){ .lines.stackable .sep{display:none} }

/* The outbox banner's retry control. As a bare .link inside the flash flex row it was
   squeezed to 27px wide and wrapped its label down the page — 27x67, caught by the
   harness on the first run after it was written. It is the only way to force a send on a
   flaky connection, so it gets a real target. */
#outbox{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2)}
/* ANY display: rule must exempt [hidden]. The UA stylesheet's [hidden]{display:none} has
   almost no specificity, so #outbox (1,0,0) beat it and the banner showed permanently
   reading "0 change(s) saved on this phone" — announcing a queue that was empty. This
   file already documents the same trap for stackable rows; I walked into it anyway. */
#outbox[hidden]{display:none}
#outbox [data-outbox-retry]{
  flex:0 0 auto;white-space:nowrap;min-height:44px;padding:0 var(--s3);
  font-weight:640}

/* Change orders listed under the job they amend. */
.colist{margin-top:var(--s5)}
.colist h3{margin:0 0 var(--s2)}
.colist p{margin:0 0 var(--s4);font-size:var(--t-sm);max-width:62ch}

/* ---------------------------------------------------------------- notices
 *
 * These were full-width blocks stacked above every page, permanently, pushing the actual
 * content down — two of them on the dashboard, one announcing an empty queue. A standing
 * condition ("you have not set a store") is not news, and news that never goes away stops
 * being read at all.
 *
 * So they are compact, quieter than a flash, and dismissible. The setup notice comes back
 * on the next visit if it is still true, because it IS still true — dismissing it is
 * "not now", not "never".
 */
.notice-bar{display:flex;align-items:flex-start;gap:var(--s3);
  padding:10px var(--s4);margin:0 0 var(--s3);border-radius:var(--r);
  background:var(--flag-wash);color:var(--flag);font-size:var(--t-sm);
  border:1px solid color-mix(in srgb,var(--flag) 22%,transparent)}
.notice-bar p{margin:0;flex:1}
.notice-bar a{color:inherit;font-weight:640;text-decoration:underline}
.notice-bar .dismiss{flex:0 0 auto;background:none;border:0;cursor:pointer;
  color:inherit;opacity:.65;font-size:1.05rem;line-height:1;padding:6px;
  /* 44, not 32. Dismissing a notice is a one-handed tap on a phone, and the harness
     measures this button directly. */
  min-width:44px;min-height:44px}
.notice-bar .dismiss:hover{opacity:1}
.notice-bar[hidden]{display:none}

/* App pages are a tool, not a brochure. The marketing footer — the tagline, the pricing
   promise, the theme switch — belongs on the pages a visitor reads, not under a
   contractor's dashboard, where it is a screen of dead space below the work. */
body[data-app] .foot{display:none}

/* ---------------------------------------------------------------- dashboard
 *
 * One hero figure, three supporting ones, then what needs doing. The old layout was four
 * equal tiles and a table of everything recent — it told a contractor what exists rather
 * than what to act on, and nothing on it was more important than anything else.
 */
.dash-head{display:flex;justify-content:space-between;align-items:flex-end;
  gap:var(--s5);flex-wrap:wrap;padding:var(--s8) 0 var(--s5)}
.dash-head h1{font-size:var(--t-h1);margin:var(--s1) 0 0}
.dash-actions{display:flex;gap:var(--s2);flex-wrap:wrap}
.dash-actions form{display:contents}

.dash-grid{display:grid;gap:var(--s4);grid-template-columns:minmax(0,1.15fr) minmax(0,2fr);
  align-items:stretch;margin-bottom:var(--s6)}
.dash-hero{display:flex;flex-direction:column;justify-content:center;gap:6px;
  padding:var(--s6);border-radius:var(--r-lg);text-decoration:none;
  background:var(--surface);border:1px solid var(--line);box-shadow:var(--e1)}
.dash-hero .kicker{color:var(--ink-3)}
/* The one number worth display type. clamp so it never outgrows a narrow card. */
.dash-hero .num{font:700 clamp(2rem, 5.5vw, 2.9rem) var(--sans);letter-spacing:-.035em;
  color:var(--ink);font-variant-numeric:tabular-nums;line-height:1.05}
.dash-hero:hover{border-color:var(--brand-ring)}
.dash-hero .overdue{font:640 var(--t-sm) var(--sans)}

.dash-side{display:grid;gap:var(--s4);
  grid-template-columns:repeat(auto-fit,minmax(140px,1fr))}
.dash-side .stat{text-decoration:none}
.dash-side .stat:hover{border-color:var(--brand-ring)}

.needs{margin-top:0}
.needs-grid{display:grid;gap:var(--s4);
  grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}
.needs-grid h3{margin:0 0 var(--s3);font-size:var(--t-md)}
.tightlist{list-style:none;margin:0;padding:0;font-size:var(--t-sm)}
.tightlist li{display:flex;align-items:baseline;gap:var(--s3);flex-wrap:wrap;
  padding:8px 0;border-top:1px solid var(--line)}
.tightlist li:first-child{border-top:0}
.tightlist li a{font-weight:640;flex:0 0 auto}
.tightlist li .muted{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
.tightlist li .num{flex:0 0 auto;font-variant-numeric:tabular-nums}

@media (max-width:820px){
  /* The hero stops sharing a row with its context and takes the full width. */
  .dash-grid{grid-template-columns:1fr}
  .dash-head{padding-top:var(--s6);align-items:flex-start}
  .dash-actions{width:100%}
  .dash-actions .btn{flex:1;text-align:center;min-height:44px}
}

/* The counter-run bar on a saved document. */
.orderbar{display:flex;align-items:center;gap:var(--s2);flex-wrap:wrap;
  margin-bottom:var(--s4);font-size:var(--t-sm)}

/* Admin console: the tier control is three fields in a table cell. */
.tierform{display:flex;gap:4px;align-items:center;flex-wrap:wrap}
.tierform select{width:auto;min-width:6rem;padding:6px 8px;font-size:var(--t-xs)}
.tierform input{width:7rem;padding:6px 8px;font-size:var(--t-xs)}
