/* One stylesheet, no build step, no framework, and no CDN.
   That last one is a product decision, not a shortcut: this app has to open on a
   phone at the foot of a ladder, on a job site, on whatever signal is going.
   Every graphic is inline SVG, and the one typeface below is served from OUR
   static directory — never from a font CDN.

   Only English text may appear in generated content. */

/* ---------------------------------------------------------------------------
   TYPEFACE — IBM Plex Sans, self-hosted.

   The founder's verdict on the system-font build (21 Aug): "the whole site
   feels bot-made". He was pointing at something real: system-ui is the default
   face of every unstyled AI-generated page, and the reader has seen a hundred
   of them. The design-system pass picked IBM Plex Sans for this product
   category (insurance paperwork wants the voice of a bank, not a startup).

   One variable woff2, 45 KB, latin subset, weights 100-700 in a single file —
   cheaper than most hero images. font-display: swap keeps the old rule alive:
   on a dead signal the page renders instantly in the system stack below and
   swaps when the file lands. Nothing external is ever requested.
   --------------------------------------------------------------------------- */
@font-face {
  font-family: "IBM Plex Sans";
  src: url("plex-sans-latin.woff2") format("woff2");
  font-weight: 100 700;
  font-style: normal;
  font-display: swap;
}

/* ---------------------------------------------------------------------------
   COLOUR

   Deep navy carries the product; it is the colour of the documents this thing
   exists to produce, and it stays out of the way of the three colours that have
   to mean exactly one thing each:

     amber  = look at this, nothing is broken   (unread photos, money owed,
                                                 an identifier spotted in a file)
     green  = settled, nothing owed
     red    = refused, stopped, wrong

   Those three are load-bearing. The accent is never allowed to borrow them, which
   is why it is not the orange a roofing product would reach for first.
   --------------------------------------------------------------------------- */
:root {
  --ink: #10151c;
  --ink-soft: #39434f;
  /* Measured 2026-08-19 in the browser: at #667281 the small print under the
     price cleared WCAG AA by 0.06 (4.56 against 4.5). That is a pass on paper
     and a coin toss on a phone in daylight, which is exactly where this page
     gets read. Darkened for headroom. */
  --muted: #5b6675;
  --line: #dfe4ea;
  --line-soft: #edf0f3;
  --bg: #f5f7f9;
  --card: #ffffff;
  --card-sunk: #fafbfc;

  --accent: #14507d;
  --accent-strong: #0e3a5c;
  --accent-soft: #e9f1f8;
  --accent-ink: #14507d;

  --ok: #1d6b3a;
  --ok-ink: #1d6b3a;
  --ok-soft: #edf8f1;
  --warn: #7a5300;
  --warn-ink: #7a5300;
  --warn-bg: #fdf6e6;
  --warn-line: #d7a03a;
  --error: #a3242b;
  --pending: #6b7683;

  --radius: 10px;
  --radius-sm: 6px;
  --shadow: 0 1px 2px rgba(16, 21, 28, 0.05), 0 1px 3px rgba(16, 21, 28, 0.04);
  --shadow-lift: 0 4px 12px rgba(16, 21, 28, 0.08);

  /* The page shell. Read by .page AND .topbar-inner so the top row and the
     content below it cannot drift apart — they did, by 217px, until 21 Aug. */
  --shell: 62rem;

  /* -------------------------------------------------------------------------
     TYPE SCALE — measured 2026-08-21: this file held 68 font-size
     declarations across 24 distinct values, none derived from any other, and
     the five section headings of the landing page sat at three different
     sizes. That is the mechanical content of "everything is a different
     size". One base (16px), one ratio (1.2 minor third), nine steps.

     🔴 No font-size is written in this file except through these tokens (the
     hero clamp is the one exception, and a test walks the file to keep it
     that way).

     --fs-xs is for UPPERCASE MICRO-LABELS ONLY — never a sentence.
     ------------------------------------------------------------------------- */
  --fs-xs:   0.8125rem;  /* 13px */
  --fs-sm:   0.9375rem;  /* 15px */
  --fs-base: 1rem;       /* 16px */
  --fs-md:   1.0625rem;  /* 17px */
  --fs-lg:   1.25rem;    /* 20px */
  --fs-xl:   1.5rem;     /* 24px */
  --fs-2xl:  1.75rem;    /* 28px */
  --fs-3xl:  2.25rem;    /* 36px */
  --fs-4xl:  2.75rem;    /* 44px - landing section headings */

  /* 4px spacing grid — the same cure applied to space that the type tokens
     applied to size. */
  --sp-1: 0.25rem; --sp-2: 0.5rem; --sp-3: 0.75rem; --sp-4: 1rem;
  --sp-5: 1.5rem; --sp-6: 2rem; --sp-7: 3rem;

  --measure: 68ch;
  --font: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

/* Dark mode follows the reader, and the guard on the selector matters: the
   default "system" setting stamps nothing on the root element, so a page that
   only checked [data-theme] would ignore the reader's own preference. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ink: #e8ecf1;
    --ink-soft: #b6c0cc;
    --muted: #8c98a6;
    --line: #2a323c;
    --line-soft: #212831;
    --bg: #0d1116;
    --card: #151b23;
    --card-sunk: #11171e;

    --accent: #6ba8dc;
    --accent-strong: #8cc0ec;
    --accent-soft: #17293a;
    --accent-ink: #9cc9ea;

    --ok: #6ed295;
    --ok-ink: #6ed295;
    --ok-soft: #14251b;
    --warn: #e8b95c;
    --warn-ink: #e8b95c;
    --warn-bg: #251e10;
    --warn-line: #6b5426;
    --error: #f08b8b;
    --pending: #8c98a6;

    --shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
    --shadow-lift: 0 4px 14px rgba(0, 0, 0, 0.4);
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font: 16px/1.6 var(--font);
  color: var(--ink);
  background: var(--bg);
  -webkit-text-size-adjust: 100%;
}

/* ---------------------------------------------------------------------------
   LINKS

   Every link the templates did not hand a class to used to fall through to the
   browser default: #0000EE, underlined. Measured 2026-08-20 in the browser on
   the live site — 62 links across the app, including the package download
   links inside the product and every link in the footer.

   It survived this long because it is INVISIBLE in light mode: #0000EE on
   #f5f7f9 clears 8:1 and merely looks dated. On the dark palette the same
   colour lands on #0d1116 at 2.0:1, well under the 4.5 floor. The page looked
   finished and one region had silently reverted to 1998.

   --accent-ink is the token that already carries this job: 7.7:1 in light,
   10.8:1 in dark.
   --------------------------------------------------------------------------- */
a {
  color: var(--accent-ink);
  text-underline-offset: 0.15em;
  text-decoration-thickness: 1px;
}

/* Underlines stay inside running text — WCAG 1.4.1 asks for them there so the
   link is not signalled by colour alone. A standalone footer or nav link is
   already marked by its position, and the underline only adds noise. */
.topbar a,
.footer a,
.footer-legal a,
.legal-nav a { text-decoration: none; }

/* Measured 2026-08-20 at 375px: the legal links in the footer were 20px tall,
   under the 24px floor in WCAG 2.5.8. The padding is horizontal as well,
   because with small targets sitting side by side, hitting the wrong one is
   more annoying than missing altogether. */
.footer a, .footer-legal a, .legal-nav a {
  display: inline-block;
  padding: 0.35rem 0.2rem;
}

/* A link that is the only content of a paragraph is not a link inside a
   sentence — it is an action in its own right: "Already have an account? Sign
   in.", "Back to the job", the single way out of the error page. The inline
   exception in WCAG 2.5.8 does not reach these, so they owe the 24px floor.

   Written with :only-child rather than a list of classes, so a "go back" link
   added next year is covered without anyone remembering this rule. */
p > a:only-child {
  display: inline-block;
  padding: 0.35rem 0.2rem;
}

.topbar a:hover,
.footer a:hover,
.footer-legal a:hover,
.legal-nav a:hover { text-decoration: underline; }

/* The bar spans the window so its underline reaches both edges; the row inside
   it sits on the same grid as the page, so the mark lines up with the headline
   underneath.

   Measured 2026-08-21 at 1440px: the mark started at x=20 and the headline at
   x=237, because .topbar was full width while .page was centred at 62rem. None
   of the content was broken — the top row simply was not on the same grid as
   everything below it, which is the kind of thing a reader notices without
   being able to name. */
.topbar {
  /* Sticky, because past the first screen there was no path to any action:
     measured 2026-08-21, the page held no CTA between the hero and the
     closing card ~3000px later. Both direct competitors pin their bar.
     z-index clears the cards' shadows; nothing else on the page stacks. */
  position: sticky;
  top: 0;
  z-index: 20;
  padding: 0.7rem 0;              /* no side padding: the row inside owns it */
  background: var(--card);
  border-bottom: 1px solid var(--line);
}

/* Same max-width AND the same side padding as .page, so the two agree to the
   pixel. Both read --shell; one number, one place. */
.topbar-inner {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  max-width: var(--shell);
  margin: 0 auto;
  padding: 0 1.25rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 650;
  font-size: var(--fs-md);
  letter-spacing: -0.012em;
  color: var(--accent-ink);
  text-decoration: none;
}

/* ---------------------------------------------------------------------------
   TOP NAVIGATION

   Measured 2026-08-20 on the live site: the bar held a mark, a name and a
   tagline, and then a wide empty stretch all the way to the right edge. Ten
   published posts were unreachable from it, and the FAQ could only be found by
   scrolling the landing page. That empty stretch is the most valuable strip on
   the page.

   `margin-left: auto` on the nav pushes the whole group right and leaves the
   brand alone on the left, which is the arrangement people already know how to
   read.
   --------------------------------------------------------------------------- */
/* The nav clusters WITH the brand; only the account block floats right.
   Measured 2026-08-21: both .topnav and the older .topbar-account rule carried
   `margin-left: auto`, so flexbox split the free space between the two auto
   margins and the nav hung mid-bar, attached to nothing. One auto margin per
   row — it lives on .topbar-account. */
.topnav {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  margin-left: 1.4rem;
  font-size: var(--fs-base);
}

.topnav a {
  display: inline-block;
  padding: 0.4rem 0.15rem;   /* 24px floor, WCAG 2.5.8 */
  color: var(--ink-soft);
  text-decoration: none;
  white-space: nowrap;
}

.topnav a:hover { color: var(--accent-ink); text-decoration: underline; }

.topbar-account {
  margin-left: auto;   /* the row's ONE auto margin — see .topnav above */
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

/* The bar's own copy of the primary action. Shorter than the one in the page
   body: at this size a full-height button would out-shout the headline, which
   is the sentence that actually has to land. */
.primary.compact {
  padding: 0.42rem 0.85rem;
  font-size: var(--fs-base);
}

/* Under ~820px the four nav links and the two account controls cannot share a
   line with the brand. The tagline goes first because the headline underneath
   says the same thing better; the nav wraps to its own row rather than
   disappearing, because a link nobody can reach is the defect we just fixed. */
@media (max-width: 820px) {
  .topbar-inner { flex-wrap: wrap; row-gap: 0.35rem; }
  .topbar .tagline { display: none; }
  .topnav { margin-left: 0; order: 3; width: 100%; gap: 0.9rem; }
  .topbar-account { margin-left: auto; }
}

/* The mark scales with the text around it rather than sitting at a fixed pixel
   size, so it keeps its proportion when the reader has enlarged type. */
.brand-mark {
  width: 1.45em;
  height: 1.45em;
  flex: none;
  display: block;
}

.brand-word { white-space: nowrap; }

/* The tagline is the first sentence a stranger reads, but it is not worth a line
   of its own on a phone: at that width the brand and the sign-out control are
   what the returning customer came for. */
.tagline {
  color: var(--muted);
  font-size: var(--fs-sm);
  border-left: 1px solid var(--line);
  padding-left: 0.9rem;
}

.page { max-width: var(--shell); margin: 0 auto; padding: 1.5rem 1.25rem 3rem; }
/* A real footer: brand, product, resources, legal — four columns on a desk,
   stacked on a phone. The single-line version read as a page that ran out of
   effort (the founder's words, 21 Aug: plain, unfinished). */
.footer {
  margin-top: var(--sp-7);
  border-top: 1px solid var(--line);
  background: var(--card);
  color: var(--muted);
  font-size: var(--fs-sm);
}
.footer-inner {
  max-width: var(--shell);
  margin: 0 auto;
  padding: var(--sp-6) 1.25rem var(--sp-5);
}
.footer-grid {
  display: grid;
  /* auto-fit: signed-in pages drop the Product column and the remaining
     columns take the room — no empty track, no second grid class. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
  gap: var(--sp-6);
}
@media (min-width: 721px) {
  .footer-brand { grid-column: span 1; max-width: 24rem; }
}
@media (max-width: 720px) {
  .footer-grid { grid-template-columns: 1fr 1fr; }
  .footer-brand { grid-column: 1 / -1; }
}
.footer-brand .brand { color: var(--accent-ink); }
.footer-brand p { margin: 0.6rem 0 0; max-width: 30ch; }
.footer-col { display: flex; flex-direction: column; align-items: flex-start; }
.footer-head {
  margin: 0 0 0.35rem;
  font-size: var(--fs-xs);
  font-weight: 650;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.footer-col a { color: var(--ink-soft); }
.footer-col a:hover { color: var(--accent-ink); }
.footer-note {
  margin: var(--sp-6) 0 0;
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line-soft);
}

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--sp-5) 1.75rem;
  margin-bottom: var(--sp-6);
  box-shadow: var(--shadow);
}

/* ---------------------------------------------------------------------------
   BANDS — the page's only rhythm device.

   Measured 2026-08-21: every section on the landing page was the same .card —
   same white box, same width, same shadow, five times in a row. A reader
   scrolling down was being told new things but never SHOWN a new shape, so the
   page peaked at the headline and flattened. That is most of what "it looks
   wrong from the start" means; the content underneath was never the problem.

   A band sits BACK where a card sits UP: sunken ground, a hairline instead of
   a shadow, and more vertical air. Card–band–card–band is the whole trick.

   🔴 DELIBERATELY NOT FULL-BLEED. The first version pulled the band to the
   viewport edge with `margin: calc(50% - 50vw)` and percentage padding — and
   the live page came back with both bands crushed into a word-per-line column
   (measured 2026-08-21, full-page capture). Full-bleed via negative margins
   depends on assumptions about the shell that this stylesheet should not be
   making. The plain version cannot break, and the rhythm survives: what the
   eye needs is the change of ground, not the extra 200px of background.
   --------------------------------------------------------------------------- */
.band {
  background: var(--card-sunk);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  padding: var(--sp-7) 1.75rem;
  margin: var(--sp-6) 0;
  box-shadow: none;
}

/* Same reading-measure cap .card children get; a band is a card that sits
   back, not a different kind of container. */
.band > p,
.band > ul,
.band > ol { max-width: var(--measure); }

/* Type scale. Headings are set tight and dark; body copy is capped at a readable
   measure so a paragraph on a wide desktop does not run to 200 characters. */
h1 {
  font-size: var(--fs-xl);
  line-height: 1.25;
  letter-spacing: -0.02em;
  margin: 0 0 0.6rem;
}
h2 {
  font-size: var(--fs-xl);
  line-height: 1.3;
  letter-spacing: -0.012em;
  margin: 0 0 0.6rem;
}
/* The line above the headline that names the reader. Set in the accent so it
   reads as a label rather than a first sentence, and tracked out so it does not
   compete with the headline for the same shape. */
.eyebrow {
  margin: 0 0 0.6rem;
  font-size: var(--fs-xs);
  font-weight: 650;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--accent-ink);
}

/* Measured 2026-08-21: h3 was 0.98rem = 15.68px, against 16px body text. A
   heading set SMALLER than the text it introduces stops working as a heading,
   and the eye reads the whole block as one grey field. That is the mechanical
   reason "What makes it different" — four ~55-word paragraphs under four such
   headings — read as a wall of text rather than four separate claims.

   Nothing was cut to fix it. The hierarchy was simply missing. */
h3 {
  font-size: var(--fs-lg);
  font-weight: 650;
  color: var(--ink);
  letter-spacing: -0.006em;
  line-height: 1.35;
  margin: 1.4rem 0 0.45rem;
}

.card > p,
.card > ul,
.card > ol { max-width: var(--measure); }

.lede { margin-top: 0; }
.muted { color: var(--muted); }
.ok { color: var(--ok); }
.error { color: var(--error); }

.notice { max-width: 60rem; margin: 0 auto; padding: 0 1.25rem; }
.notice:empty { padding: 0; }

button, .primary-link {
  font: inherit;
  font-weight: 550;
  padding: 0.52rem 0.95rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
  transition: background-color 0.12s ease, border-color 0.12s ease;
}
button:hover:not([disabled]) { background: var(--card-sunk); border-color: var(--muted); }
/* a.primary and button.primary must look identical.
   Measured 2026-08-18: the rule existed only for `button.primary`, but
   `<a class="primary">` was already used for "Open the photo plan" on the job
   screen and "Download the photo plan" on the shot list. Both rendered as plain
   blue text. The most-tapped control on the one screen that is opened on a phone,
   at the foot of the ladder, was not a button.
   22 Aug 2026: the photo plan link on the job screen became a quiet line inside
   the Files card, and the first action of that screen is now a link too -
   "Review items" while the job waits for approval, "Open the package" once it is
   finished. Same rule, same reason. */
button.primary,
a.primary {
  background: var(--accent);
  color: #fff;
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 0.58rem 1.15rem;
  font: inherit;
  font-weight: 600;
  display: inline-block;
  text-decoration: none;
  cursor: pointer;
  box-shadow: var(--shadow);
  transition: background-color 0.12s ease, transform 0.06s ease;
}
button.primary:hover:not([disabled]),
a.primary:hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}
button.primary:active:not([disabled]),
a.primary:active { transform: translateY(1px); }

/* In dark mode the accent is a light blue, so white text on it fails contrast.
   The button flips to dark ink instead of quietly becoming unreadable. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) button.primary,
  :root:not([data-theme="light"]) a.primary { color: #0d1116; }
}
button[disabled] { opacity: 0.5; cursor: default; }
.primary-link { display: inline-block; text-decoration: none; color: var(--accent); }

/* Keyboard focus. There was no :focus rule at all (grep: 0 hits), and the
   buttons' own border swallowed the browser's default ring, so someone moving
   through the page with Tab could not see where they were. */
:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}



/* The shot list. The product's one measured difference, and the least styled
   screen in it: there was not a single rule for `<ol class="frames">`. The frames
   ran together as a plain numbered list — the worst possible shape for text read
   on a phone, one-handed, in sunlight, on a roof. */
ol.frames { list-style: decimal; padding-left: 2.2rem; margin: 0.5rem 0; }
ol.frames > li {
  border-top: 1px solid var(--line);
  padding: 0.7rem 0 0.7rem 0.3rem;
}
ol.frames > li:first-child { border-top: 0; }
ol.frames > li::marker { color: var(--muted); font-variant-numeric: tabular-nums; }
ol.frames strong { display: block; }
ol.frames ul { margin: 0.3rem 0 0; padding-left: 1.1rem; }

/* Mobile. There were zero @media queries (grep matched only @keyframes), so if
   anything worked on a phone it worked by accident. The shot list screen is opened
   on a phone by design — before tear-off, at the foot of the ladder. */
@media (max-width: 640px) {
  /* The upload row overflowed a 375px screen at its 256px base width. */
  form.uploadform label { min-width: 0; width: 100%; }
  form.uploadform { align-items: stretch; }

  /* Text read in sunlight does not get smaller. 12px floors are raised. */
  .progress-text, .pill, .risk-disclaimer { font-size: var(--fs-sm); }

  /* Touch target for a gloved finger. */
  button, a.primary { padding: 0.6rem 1rem; }
  a.primary { display: block; text-align: center; }

  ol.frames { padding-left: 1.6rem; }
}

/* Reduced-motion preference — the intake animation could not be turned off. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

form.uploadform { display: flex; gap: 0.5rem; align-items: center; margin: 0.4rem 0; flex-wrap: wrap; }
form.uploadform label { min-width: 16rem; }

fieldset { border: 1px solid var(--line); border-radius: 4px; margin: 0 0 1rem; padding: 0.75rem; }
legend { color: var(--muted); font-size: var(--fs-sm); padding: 0 0.35rem; }
fieldset label { display: block; margin-bottom: 0.5rem; font-size: var(--fs-base); }
fieldset label.check { display: flex; gap: 0.4rem; align-items: center; }
/* ---------------------------------------------------------------------------
   FIELDS PEOPLE TYPE INTO

   Measured 2026-08-20 on the live site at 375px wide: the sign-in and REGISTER
   fields were the browser's raw defaults — 13.33px type, 1px 2px padding, 21px
   tall — because the only input rule in this stylesheet was
   `fieldset input[type="text"]`, and those two fields are neither inside a
   fieldset nor type="text". The submit button beside them was styled, and that
   is exactly what made the form look finished.

   Two of those numbers are not cosmetic:
     · under 16px, iOS Safari ZOOMS the page when the field is tapped. This app
       is used one-handed on a roof; the page jumping on the first tap of the
       registration form is the worst possible first minute.
     · a 21px target is under the 24px floor in WCAG 2.5.8, and nowhere near
       what a gloved finger can hit.

   The selector excludes types rather than listing the ones we happen to use
   today, so a field added next year is covered without anyone remembering.
   --------------------------------------------------------------------------- */
input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
select,
textarea {
  font: inherit;                 /* 16px — stops iOS from zooming in */
  padding: 0.55rem 0.65rem;
  min-height: 44px;
  max-width: 100%;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card-sunk);
  color: var(--ink);
}

/* Kills the inner shadow iOS paints on text fields. Input and textarea only,
   so the arrow on a select survives. */
input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
textarea { -webkit-appearance: none; appearance: none; }

/* A field nested inside its label — the register form does this — should not
   sit inline. */
label > input:not([type="checkbox"]):not([type="radio"]),
label > select,
label > textarea { display: block; width: 100%; margin-top: 0.3rem; }

/* Layout only; the measurements now come from the base rule above. */
fieldset input[type="text"], fieldset select {
  display: block;
  width: 100%;
}
fieldset input[type="checkbox"] { width: auto; }

ul { margin: 0.35rem 0; padding-left: 1.1rem; }
.joblist, .uploadlist, .filelist { list-style: none; padding-left: 0; }
.joblist li, .uploadlist li { display: flex; gap: 0.75rem; align-items: baseline; padding: 0.2rem 0; }

.pill { font-size: var(--fs-xs); padding: 0.1rem 0.45rem; border-radius: 999px; background: var(--bg); border: 1px solid var(--line); }

.status dl.facts { display: grid; grid-template-columns: max-content 1fr; gap: 0.15rem 0.75rem; font-size: var(--fs-base); }
.status dl.facts dt { color: var(--muted); }
.status dl.facts dd { margin: 0; }

/* --- approval screen ---------------------------------------------------- */

.counter { display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap; margin: 0.5rem 0; }
.counter .count { font-size: var(--fs-base); padding: 0.15rem 0.5rem; border-radius: 4px; background: var(--bg); }
.counter .approved { color: var(--ok); }
.counter .pending { color: var(--pending); }

/* Group rule between batches of six on a long review list — a rest line, not
   a control. It selects nothing and submits nothing. */
.review-group-head {
  margin: 1.6rem 0 0.5rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--line);
  font-size: var(--fs-sm);
  letter-spacing: 0.02em;
}

.candidate {
  border: 1px solid var(--line);
  border-left-width: 4px;
  border-radius: 4px;
  padding: 0.75rem 1rem;
  margin-bottom: 0.75rem;
}
/* Four states, four looks. "Not decided" must never look like "left out", and
   an item already before the carrier must not look like either. */
.candidate.state-pending { border-left-color: var(--pending); }
.candidate.state-included { border-left-color: var(--ok); background: #f4fbf6; }
.candidate.state-excluded { border-left-color: var(--line); background: #fafafa; opacity: 0.75; }
.candidate.state-carried { border-left-color: var(--accent); background: #f6f7fb; }

.candidate-head { display: flex; gap: 0.6rem; align-items: baseline; flex-wrap: wrap; font-size: var(--fs-sm); }
.candidate-head .cid { font-weight: 700; }
.candidate-head .argument { color: var(--muted); }
.candidate-head .code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.candidate-head .code-blank { color: var(--muted); }
.candidate-head .state-label { margin-left: auto; font-weight: 600; }

.risk { padding: 0.05rem 0.4rem; border-radius: 3px; }
.risk-medium { background: #fdf3e0; color: var(--warn); }
.risk-high { background: #fdeaea; color: var(--error); }

.candidate .description { margin: 0.5rem 0 0.25rem; }
.candidate .why, .candidate .photos, .candidate .state-note,
.candidate .contractor-note, .candidate .policy-note, .candidate .risk-note {
  margin: 0.2rem 0; font-size: var(--fs-base);
}
.candidate .contractor-note { color: var(--accent); }
.candidate .policy-note { color: var(--warn); }
.candidate .missing { font-size: var(--fs-base); color: var(--warn); }

.decisions { display: flex; gap: 0.5rem; margin-top: 0.6rem; align-items: flex-end; flex-wrap: wrap; }
.decisions button.include { border-color: var(--ok); color: var(--ok); }
.decisions button.exclude { border-color: var(--muted); color: var(--muted); }
.decisions-locked { margin-top: 0.6rem; font-size: var(--fs-base); color: var(--accent); }

/* The quantity the contractor types. It lives INSIDE the include form, so the
   measurement travels with the decision it belongs to — one request, one item.
   Narrow on purpose: it holds a number, not a sentence. */
.qty-field { display: block; margin-bottom: 0.25rem; }
.qty-field .qty-label { display: block; font-size: var(--fs-xs); color: var(--muted); }
.qty-field input { width: 8rem; }
.qty-source { margin: 0.15rem 0 0; font-size: var(--fs-xs); color: var(--muted); max-width: 22rem; }

/* A photograph whose date does not fit this claim. The carrier sees the date in
   the exhibit; this reading of it is the contractor's alone. */
.photo-strip .photo-date-warning,
.photo-strip .photo-date-note {
  display: block; max-width: 14rem; margin-top: 0.15rem;
  font-size: var(--fs-xs); text-align: left;
}
.photo-strip .photo-date-warning { color: var(--warn); }
.photo-strip .photo-date-note { color: var(--muted); }

/* Items that go into the package with no measurement. A count, not a gate. */
.counter .count.missing-quantity { color: var(--warn); }

/* --- approval screen: detail surfaces (agent B) --------------------------- */

/* Badges next to the item id. The policy flag is a MARKER, not a verdict. */
.candidate-head .flag { padding: 0.05rem 0.4rem; border-radius: 3px; font-size: var(--fs-xs); }
.candidate-head .policy-flag { background: #fdf3e0; color: var(--warn); }
.candidate-head .carried-flag { background: #eef0f8; color: var(--accent); }

.candidate .code-reason, .candidate .prior-note, .candidate .carried-note {
  margin: 0.2rem 0; font-size: var(--fs-base); color: var(--muted);
}
.candidate .carried-note { color: var(--accent); }

/* Photo anchors. Small on purpose: the preview confirms which frame is
   attached, it is not the place to judge the damage. */
.photo-strip { display: flex; gap: 0.5rem; flex-wrap: wrap; list-style: none; padding: 0; margin: 0.3rem 0; }
.photo-strip .photo { text-align: center; font-size: var(--fs-xs); }
.photo-strip img { width: 96px; height: 96px; object-fit: cover; border: 1px solid var(--line); border-radius: 3px; display: block; }
.photo-strip .photo-placeholder {
  display: flex; align-items: center; justify-content: center;
  width: 96px; height: 96px; border: 1px dashed var(--line); border-radius: 3px; color: var(--muted);
}
.photo-strip .photo-id { display: block; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }

/* Rejection risk. No percentage is rendered anywhere in this block. */
.risk-block { margin: 0.5rem 0; border: 1px solid var(--line); border-radius: 4px; padding: 0.4rem 0.6rem; font-size: var(--fs-base); }
.risk-block summary { cursor: pointer; display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.risk-block .risk-title { font-weight: 600; }
.risk-block .risk-evidence { color: var(--muted); font-size: var(--fs-sm); }
.risk-block ul { margin: 0.2rem 0 0.5rem 1.1rem; padding: 0; }
.risk-block .risk-raised { color: var(--warn); }
.risk-block .risk-supported { color: var(--ok); }
.risk-block .risk-disclaimer { color: var(--muted); font-size: var(--fs-xs); margin: 0.3rem 0 0; }

/* The internal block must read as a different document from the rest of the
   row: everything above it can go to the carrier, nothing inside it can. */
.internal-block {
  margin: 0.6rem 0 0; padding: 0.5rem 0.7rem;
  border: 1px dashed var(--warn); border-radius: 4px; background: #fffdf7;
}
.internal-block .internal-title { margin: 0; font-size: var(--fs-xs); letter-spacing: 0.03em; color: var(--warn); }
.internal-block .internal-intro { margin: 0.2rem 0 0.4rem; font-size: var(--fs-xs); color: var(--muted); }
.internal-block p { margin: 0.2rem 0; font-size: var(--fs-base); }
.internal-hint { font-size: var(--fs-sm); color: var(--muted); }

/* View bar. These controls change what is SHOWN, never what is approved. */
.view-bar { border-bottom: 1px solid var(--line); padding-bottom: 0.6rem; margin-bottom: 0.8rem; }
.view-group { display: flex; gap: 0.4rem; align-items: baseline; flex-wrap: wrap; margin-bottom: 0.3rem; }
.view-group .view-label { font-size: var(--fs-xs); color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.view-option { font-size: var(--fs-sm); padding: 0.1rem 0.45rem; border: 1px solid var(--line); border-radius: 3px; text-decoration: none; }
.view-option.active { border-color: var(--accent); color: var(--accent); font-weight: 600; }
.view-note, .view-hidden { font-size: var(--fs-xs); color: var(--muted); margin: 0.3rem 0 0; }

.notice-block { border: 1px solid var(--line); border-radius: 4px; padding: 0.6rem 0.9rem; margin-bottom: 0.75rem; font-size: var(--fs-base); background: var(--card); }
.notice-block h2 { font-size: var(--fs-lg); margin: 0 0 0.3rem; }
.notice-block ul { margin: 0 0 0 1.1rem; padding: 0; }
.carried-summary { border-color: var(--accent); background: #f6f7fb; }

.dropped li, .questions li, .warnings li { font-size: var(--fs-base); margin-bottom: 0.2rem; }
.dropped .argument { font-weight: 600; }
.dropped .reason { color: var(--muted); }

/* Grouped by reason (measured 2026-08-21: the flat list ran to 22 rows and
   repeated the same sentence twenty times; the review page hit 12.7 screens on
   a phone). The sentence leads the group once; the arguments sit under it. */
.dropped-group { margin-top: 0.9rem; }
.dropped-group > .reason {
  margin: 0 0 0.3rem;
  max-width: var(--measure);
}
.dropped-group ul { margin: 0; }
.dropped-group .detail { color: var(--muted); font-size: var(--fs-sm); }
.dropped-group .detail::before { content: "— "; }
.filelist.internal a { color: var(--warn); }

/* --- intake screen (agent A) --------------------------------------------- */
/* Upload box: drop target, the limits written out, and a progress bar. */
/* The camera path on a phone. Styled as a secondary button so it reads as an
   action, not a form label; the input it triggers is visually hidden but stays
   in the accessibility tree (display:none would remove it from keyboard and
   screen-reader reach). */
.camera-button {
  display: inline-block;
  padding: 0.45rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card-sunk);
  cursor: pointer;
  font-size: var(--fs-base);
}
.camera-button:hover { border-color: var(--muted); }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.uploadform {
  border: 1px dashed var(--line);
  border-radius: 6px;
  padding: 0.7rem 0.8rem;
  margin-top: 0.7rem;
}
.uploadform.dragging { border-color: var(--accent); background: #f2f7ff; }
.uploadform > label { font-weight: 600; display: block; margin-bottom: 0.2rem; }
.uploadform .limits { margin: 0.1rem 0 0.45rem; font-size: var(--fs-sm); }
.uploadform .drophint { margin: 0.35rem 0 0.5rem; font-size: var(--fs-sm); }

/* Refused files sit in their own titled block. Loose lines mixed into the rest
   of the list read as "accepted", and nothing here may be mistaken for that. */
.rejected-block {
  border-left: 3px solid var(--error);
  padding-left: 0.6rem;
  margin: 0.5rem 0;
}
.rejected-block .error-title { font-weight: 600; color: var(--error); margin: 0 0 0.25rem; }

/* Progress. htmx injects its own indicator rule using opacity, which hides the
   bar but still reserves its height, leaving an empty strip under every box.
   Visibility is set here instead: htmx puts the htmx-request class on whatever
   hx-indicator points at, for as long as the request runs. */
.progress.htmx-indicator { display: none; }
.progress.htmx-indicator.htmx-request { display: block; }

.progress {
  position: relative;
  height: 1.1rem;
  background: #eef0f3;
  border-radius: 999px;
  overflow: hidden;
  margin-top: 0.4rem;
}
.progress-bar {
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width 0.15s linear;
}
/* When the browser cannot compute a total, no percentage is invented: the bar
   runs indeterminate instead. */
.progress-bar.indeterminate {
  width: 40%;
  animation: intake-slide 1.1s ease-in-out infinite;
}
@keyframes intake-slide {
  0% { margin-left: -40%; }
  100% { margin-left: 100%; }
}
/* The step list on the waiting screen. Done steps carry a check drawn in CSS
   (no icon font, no image — this page must render on a dead signal). */
.progress-steps { list-style: none; margin: 0.5rem 0; padding: 0; }
.progress-steps li { padding: 0.2rem 0 0.2rem 1.5rem; position: relative; }
.progress-steps li::before {
  position: absolute; left: 0.15rem;
}
.step-done::before { content: "✓"; color: var(--ok); font-weight: 700; }
.step-active { font-weight: 600; }
.step-active::before {
  content: ""; width: 0.55em; height: 0.55em; border-radius: 50%;
  background: var(--accent); top: 0.55em;
  animation: pulse 1.2s ease-in-out infinite;
}
.step-waiting::before { content: "·"; }
@keyframes pulse { 50% { opacity: 0.35; } }
@media (prefers-reduced-motion: reduce) { .step-active::before { animation: none; } }

.progress-text {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-xs);
  color: var(--ink);
}

/* A required identity box is marked while it is still missing. */
.field-missing input { border-color: var(--error); }
.req { font-weight: 400; font-size: var(--fs-xs); color: var(--muted); }

/* Facet counts by elevation: four small number boxes side by side. */
.elevations { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.elevations .elevation { flex: 0 0 auto; }
.elevations .elevation input { width: 5rem; }

/* --- run screen: progress, failure, budget, cost, package (agent C) -------- */

/* Each block inside the run card is separated by a rule rather than a colour:
   the card is read top to bottom and the sections must not compete.

   The run block is .run-progress, not .progress. Two rules used to claim
   .progress — the upload pill and this block — and with border-box sizing they
   cancelled: 17.6px of fixed height minus 13.6px of padding minus a 1px border
   left three drawable pixels under a line that read as a section rule. The step
   list, the elapsed time and the "you can close the tab" sentence were on the
   page and invisible. The upload pill kept the name; this block took its own.

   .cost is gone with the card it styled: the run-cost block was removed from
   the contractor's screen (see partials/status.html). */
.run-progress,
.failure,
.budget,
.package {
  margin-top: 1rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--line);
}
.run-progress h3,
.failure h3,
.budget h3,
.package h3 {
  margin: 0 0 0.4rem;
  font-size: var(--fs-lg);
}
/* The package is a card of its own now (step 4), so it does not need the rule
   that separated it from the blocks above it inside the run card. */
#package-card > .package {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

.progress-step { margin: 0.2rem 0; }
.step-count {
  display: inline-block;
  min-width: 5.5rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.step-name { font-weight: 600; }

.failure { border-left: 3px solid var(--error); padding-left: 0.75rem; }
.failure .detail { margin-top: 0.5rem; }
.failure summary { cursor: pointer; color: var(--muted); font-size: var(--fs-base); }
/* The technical line wraps instead of stretching the card sideways. */
.detail-line {
  margin: 0.4rem 0 0;
  padding: 0.5rem;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 3px;
  font-size: var(--fs-xs);
  white-space: pre-wrap;
  word-break: break-word;
}

.warning { color: var(--warn); }
.gone { color: var(--muted); text-decoration: line-through; }

/* The two file groups never share a frame. The carrier group and the group that
   must NOT reach the carrier are told apart by a heading and a border, not by a
   colour alone. */
.filegroup {
  margin-top: 0.75rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: 4px;
}
.filegroup.carrier { border-left: 3px solid var(--ok); }
.filegroup.internal { border-left: 3px solid var(--warn); background: #fffdf6; }
.filegroup h4 { margin: 0 0 0.25rem; font-size: var(--fs-base); }
.filelist { margin: 0.4rem 0 0; padding-left: 1.1rem; }
.filelist li { margin: 0.2rem 0; }
.filelist .muted { font-size: var(--fs-xs); margin-left: 0.4rem; }

/* The run-cost block and its .cost rules are gone: on a hosted service the
   model is called with our key and the contractor pays a flat fee per file, so
   the call counts and the dollar figure were noise on their screen and our own
   unit economics on everyone else's. The numbers stay on the record and in the
   run log. */

.events { margin-top: 1rem; padding-top: 0.85rem; border-top: 1px solid var(--line); }
.events summary { cursor: pointer; font-weight: 600; }
.eventlist { margin: 0.5rem 0 0; padding-left: 1.2rem; font-size: var(--fs-sm); }
.eventlist li { margin: 0.15rem 0; }
.event-at { color: var(--muted); font-variant-numeric: tabular-nums; }
.event-status { font-weight: 600; margin: 0 0.35rem; }


/* -------------------------------------------------------------------------
   Top bar account controls.
   The sign-out control has to be a POST (a GET link would let any page sign
   the user out by embedding an image). It is styled to read as a link so the
   bar does not gain a heavy button.
   ------------------------------------------------------------------------- */
/* (Layout for .topbar-account lives with the nav rules near the top of the
   file. This block existed as a SECOND definition carrying its own
   `margin-left: auto`; with the nav's auto margin it split the bar's free
   space and left the nav floating mid-air. Measured and merged 2026-08-21.) */

.linklike {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--muted, #5a6472);
  cursor: pointer;
  text-decoration: underline;
}

.linklike:hover,
.linklike:focus-visible {
  color: var(--ink, #101418);
}


/* -------------------------------------------------------------------------
   Identifiers spotted inside an uploaded file.
   Deliberately amber, not red: the file was accepted. Red is reserved for
   uploads that were refused, and blurring that line would make the
   contractor re-upload a file that is already in place.
   ------------------------------------------------------------------------- */
.sensitive-block {
  border: 1px solid var(--warn-line, #d7a03a);
  background: var(--warn-bg, #fdf6e6);
  border-radius: 6px;
  padding: 0.75rem 1rem;
  margin: 0.75rem 0;
}

.warn-title {
  font-weight: 700;
  margin: 0 0 0.35rem;
  color: var(--warn-ink, #7a5300);
}

ul.sensitive {
  margin: 0;
  padding-left: 1.1rem;
}

li.warn {
  color: var(--warn-ink, #7a5300);
}


/* -------------------------------------------------------------------------
   Footer legal links and the legal pages themselves.
   ------------------------------------------------------------------------- */




.legal {
  max-width: 46rem;
}

.legal h2 {
  margin-top: 1.6rem;
  font-size: var(--fs-lg);
}

.legal li {
  margin-bottom: 0.35rem;
}

.legal-nav {
  margin-top: 2rem;
  padding-top: 1rem;
  border-top: 1px solid var(--line, #e2e6ea);
}


/* -------------------------------------------------------------------------
   What this file costs, shown before the button is pressed.
   Green when nothing is owed, amber when it is. Neither is red: owing money
   is not an error, and red here would read as "something went wrong".
   ------------------------------------------------------------------------- */
.charge-free {
  color: var(--ok-ink, #1d6b3a);
  font-weight: 600;
}

.charge-due {
  color: var(--warn-ink, #7a5300);
  font-weight: 600;
}

/* ===========================================================================
   THE PUBLIC PAGE

   A signed-out visitor lands here; a signed-in customer never sees it. The
   layout has one job: get a sceptical roofer from "what is this" to "I will
   try one file" without a single claim we cannot stand behind.
   =========================================================================== */

.hero {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  gap: 2.5rem;
  align-items: center;
  padding: 1.5rem 0 2.5rem;
}

.hero h1 {
  font-size: clamp(2.25rem, 1.4rem + 3.2vw, 3.5rem);
  line-height: 1.16;
  letter-spacing: -0.028em;
  margin: 0 0 1rem;
  text-wrap: balance;
}

/* Landing-page reading size: one step up from the panel. The landing page is
   read at arm's length by a stranger; the panel is worked in by a customer.
   Same scale, different step. */
/* ---------------------------------------------------------------------------
   MOTION — the founder asked for it by name (21 Aug: the page "does not do
   anything as you scroll"). Everything here obeys three rules from the design
   pass: 150-300ms, ease-out on entry, and reduced-motion turns ALL of it off.

   The hiding style lives ONLY under html.js-reveal, which only reveal.js
   adds — no JS, no reduced-motion, no IntersectionObserver: nothing is ever
   hidden. Failing open is the whole design.
   --------------------------------------------------------------------------- */
.js-reveal .landing > section {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 280ms ease-out, transform 280ms ease-out;
}
.js-reveal .landing > section.revealed {
  opacity: 1;
  transform: none;
}

/* One-shot entrance for the first paint: copy first, the output card a beat
   behind it — cause, then evidence. Runs once, never on scroll. */
@media (prefers-reduced-motion: no-preference) {
  .hero-copy { animation: rise 360ms ease-out both; }
  .hero-art  { animation: rise 360ms ease-out 120ms both; }
  @keyframes rise {
    from { opacity: 0; transform: translateY(12px); }
    to   { opacity: 1; transform: none; }
  }
}

/* A quiet ground behind the hero so the top of the page reads as a designed
   place rather than text on a void — the "top feels half-finished" complaint.
   Pure CSS gradient: no image, no request, both themes. */
.hero {
  position: relative;
}
.hero::before {
  content: "";
  position: absolute;
  inset: -2.5rem -3rem 0;
  background:
    radial-gradient(38rem 22rem at 18% 0%, var(--accent-soft) 0%, transparent 68%);
  opacity: 0.8;
  pointer-events: none;
  z-index: -1;
}

/* The second hero paragraph is a NOTE (who approves, whose name), not the
   pitch — one step down from the lede so the two read as different voices. */
/* ---------------------------------------------------------------------------
   AUTH CARD

   Measured 2026-08-21 (the founder's own screenshot): the sign-in form
   stretched across the full 62rem shell — an email field a metre wide, the
   password hint floating beside the submit button, and a page that read as
   unfinished. A credential form is a small, focused object; the card says so.
   --------------------------------------------------------------------------- */
.auth {
  max-width: 26rem;
  margin: 3rem auto;
  padding: 2rem 2rem 1.75rem;
}
/* The heading of the sign-in / register / password cards. h1, not h2: these
   pages carry no other heading, so as an h2 the page had no main heading at
   all (24 Aug 2026, measured with Playwright: /sign-in, /register, /forgot and
   /reset -- none of the four had an h1). Someone moving through the page by
   heading could not find out what page they were on. The size is deliberately
   unchanged: the fix is about meaning, not appearance. */
.auth h1 { font-size: var(--fs-xl); margin-bottom: 1.25rem; }
.auth label {
  display: block;
  margin-bottom: 1rem;
  font-weight: 550;
  font-size: var(--fs-sm);
}
.auth label > input { margin-top: 0.35rem; font-weight: 400; }
.auth small { display: block; margin-top: 0.4rem; font-weight: 400; }
.auth button.primary { width: 100%; margin-top: 0.5rem; }
.auth .error { margin-top: 0; }

.hero-note { font-size: var(--fs-base); color: var(--ink-soft); }

.hero-lede, .faq dd, .price-points li, .prose p, .prose li {
  font-size: var(--fs-md);
}

.hero-lede {
  font-size: var(--fs-md);
  color: var(--ink-soft);
  max-width: 34rem;
  margin: 0 0 0.9rem;
}

.hero-actions {
  display: flex;
  gap: 1.1rem;
  align-items: center;
  flex-wrap: wrap;
  margin: 1.5rem 0 0.85rem;
}

.hero-actions .primary { font-size: var(--fs-md); padding: 0.72rem 1.4rem; }

.hero-fineprint {
  color: var(--muted);
  font-size: var(--fs-sm);
  margin: 0;
  max-width: 32rem;
}

.hero-art { margin: 0; }
.hero-art svg {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--radius);
  box-shadow: var(--shadow-lift);
}
.hero-art figcaption {
  font-size: var(--fs-sm);
  margin-top: 0.75rem;
  max-width: 26rem;
}

/* Numbered steps. The number is set in the accent and hung outside the text
   column so the sentences stay in one clean line down the page. */
ol.steps {
  list-style: none;
  counter-reset: step;
  padding: 0;
  margin: 0;
  max-width: var(--measure);
}
ol.steps > li {
  counter-increment: step;
  position: relative;
  padding: 0 0 1.05rem 2.6rem;
}
ol.steps > li::before {
  content: counter(step);
  position: absolute;
  left: 0;
  top: 0.05rem;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-size: var(--fs-sm);
  font-weight: 700;
  display: grid;
  place-items: center;
}
ol.steps > li:last-child { padding-bottom: 0; }
ol.steps strong { display: block; margin-bottom: 0.15rem; }

/* Measured 2026-08-21: at the shell's 952px this resolved to THREE columns
   (17rem tracks), so the four features flowed 3+1 with a lone orphan on the
   second row. 20rem tracks cap it at two columns wide, one on a phone —
   which is the shape the content actually has: four claims, two by two. */
.grid-2 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  gap: 1.75rem 2.25rem;
}
.feature h3 { margin-top: 0; color: var(--ink); }
.feature p { color: var(--ink-soft); margin: 0; }

/* Price. The figure is large because it is the objection, and answering an
   objection quietly is the same as dodging it. */
.price-row {
  display: flex;
  gap: 2.25rem;
  align-items: flex-start;
  flex-wrap: wrap;
}
.price-tag {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  padding: 1rem 1.35rem;
  background: var(--accent-soft);
  border-radius: var(--radius);
  flex: none;
}
.price-figure {
  font-size: var(--fs-3xl);
  font-weight: 700;
  letter-spacing: -0.03em;
  color: var(--accent-ink);
}
.price-unit { color: var(--accent-ink); font-weight: 600; }
.price-points { margin: 0; padding-left: 1.15rem; max-width: 34rem; }
.price-points li { margin-bottom: 0.45rem; }

/* The FAQ carries more weight on this page than it does on most, and that is a
   judgement about this buyer rather than a style preference. A roofer deciding
   whether to spend $39.99 on a tool he has never heard of is not asking to be
   persuaded — he has a short list of blunt questions, and the section that
   answers them is the section that closes. It used to be set at body size and
   read as an afterthought at the bottom of the page.

   The question is set larger than the answer so the list can be SCANNED: a
   reader who came for one of these has to find it without reading the rest. */
.faq { margin: 0; max-width: var(--measure); }
.faq dt {
  font-weight: 650;
  font-size: var(--fs-lg);
  line-height: 1.4;
  margin-top: 1.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
  color: var(--ink);
}
.faq dt:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.faq dd {
  margin: 0.5rem 0 0;
  color: var(--ink-soft);
  line-height: 1.65;
}


.closing { background: var(--card-sunk); }
.closing h2 { font-size: var(--fs-lg); max-width: 32rem; }

@media (max-width: 860px) {
  .hero {
    grid-template-columns: 1fr;
    gap: 1.75rem;
    padding-top: 0.5rem;
  }
  /* The picture goes under the words on a phone. The headline is what a
     visitor arriving from an ad needs first. */
  .hero-art { order: 2; }
  .hero-copy { order: 1; }
  .hero-actions .primary { width: 100%; text-align: center; }
  .price-row { gap: 1.25rem; }
}

@media (max-width: 560px) {
  /* The tagline is dropped, not shrunk: at this width it pushes the sign-out
     control off the bar, and a customer who cannot sign out is a worse
     outcome than a customer who does not read the strapline twice. */
  .tagline { display: none; }
}

/* ===========================================================================
   BLOG

   Long-form reading, which is a different problem from the app: one column,
   a hard measure, and generous line spacing. The one thing that matters most
   here is that a contractor reading this on a phone in a truck can follow it.
   =========================================================================== */

.postlist { list-style: none; padding: 0; margin: 0; }
.postlist > li {
  padding: 1.25rem 0;
  border-top: 1px solid var(--line-soft);
}
.postlist > li:first-child { border-top: 0; padding-top: 0; }
.postlist.compact > li { padding: 0.5rem 0; }

.post-title {
  font-size: var(--fs-lg);
  font-weight: 650;
  letter-spacing: -0.012em;
  color: var(--ink);
  text-decoration: none;
  display: inline-block;
}
.post-title:hover { color: var(--accent-ink); text-decoration: underline; }

/* The three teasers on the landing page. A grid, not a list: three columns on
   a desk, one on a phone, and the titles do the selling. */
.post-teasers {
  list-style: none;
  padding: 0;
  margin: 1.2rem 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  gap: 1.5rem 2rem;
}
.post-teasers .post-desc { max-width: none; }

.post-desc { color: var(--ink-soft); margin: 0.3rem 0 0.4rem; max-width: var(--measure); }
.post-meta { color: var(--muted); font-size: var(--fs-sm); margin: 0; }

.prose { max-width: 44rem; }
.prose h1 { font-size: var(--fs-2xl); }
.prose > p,
.prose > ul,
.prose > ol,
.prose > blockquote { max-width: none; }
.prose p { margin: 0 0 1.05rem; line-height: 1.7; }
.prose h2 {
  font-size: var(--fs-xl);
  margin: 2rem 0 0.7rem;
}
.prose ul, .prose ol { margin: 0 0 1.05rem; padding-left: 1.3rem; }
.prose li { margin-bottom: 0.5rem; line-height: 1.65; }

/* Pull quotes carry the sentence a contractor would copy into the letter, so
   they are set apart rather than buried in the paragraph that explains them. */
.prose blockquote {
  margin: 1.3rem 0;
  padding: 0.9rem 1.15rem;
  border-left: 3px solid var(--accent);
  background: var(--card-sunk);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  color: var(--ink);
}
.prose blockquote p:last-child { margin-bottom: 0; }

.prose .muted {
  font-size: var(--fs-sm);
  border-top: 1px solid var(--line-soft);
  padding-top: 1rem;
  margin-top: 2rem;
}

@media (max-width: 560px) {
  .prose { max-width: none; }
  .prose p, .prose li { line-height: 1.72; }
}

/* -------------------------------------------------------------------------
   Quick Entry block — the approved items in a form that can be typed or
   pasted into an estimating package. This is what stands in for a macro file:
   the structural macro schema is not published anywhere we could verify, and
   inventing it would produce a file that looks generated and does not open.

   Numbers and codes are set in the monospace face because they are measured
   values, not prose — and because a column of figures only reads as a column
   when the digits line up.
   ------------------------------------------------------------------------- */
.quickentry {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}

table.qe {
  width: 100%;
  border-collapse: collapse;
  margin: 0.75rem 0;
  font-size: var(--fs-sm);
}

table.qe th {
  text-align: left;
  font-size: var(--fs-xs);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 700;
  padding: 0 0.6rem 0.4rem 0;
  border-bottom: 1px solid var(--line);
}

table.qe td {
  padding: 0.42rem 0.6rem 0.42rem 0;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: baseline;
}

table.qe .mono {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  white-space: nowrap;
}

/* A wide table must scroll inside its own box rather than pushing the page
   sideways — this screen gets opened on a phone. */
.quickentry table.qe { display: block; overflow-x: auto; }

.qe-plain {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  background: var(--card-sunk);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 0.85rem;
  overflow-x: auto;
  white-space: pre;
  margin: 0.6rem 0 0;
}

.quickentry summary {
  cursor: pointer;
  color: var(--accent-ink);
  font-size: var(--fs-base);
}

/* -------------------------------------------------------------------------
   Tables inside an article.
   The state-by-state tables are wider than a phone. A wide table must scroll
   inside its own box; if the page body scrolls sideways instead, every
   paragraph on the page drifts off-screen with it and the article becomes
   unreadable one-handed — which is exactly where these get read.
   ------------------------------------------------------------------------- */
.prose table {
  display: block;
  overflow-x: auto;
  width: 100%;
  border-collapse: collapse;
  margin: 1.2rem 0;
  font-size: var(--fs-sm);
}

.prose th {
  text-align: left;
  font-weight: 650;
  padding: 0.5rem 0.9rem 0.5rem 0;
  border-bottom: 2px solid var(--line);
  white-space: nowrap;
}

.prose td {
  padding: 0.5rem 0.9rem 0.5rem 0;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: top;
}

.prose tbody tr:last-child td { border-bottom: 0; }


/* ===========================================================================
   LANDING, SECOND PASS - measured against five competitor pages, 21 Aug 2026.

   What they had that we did not: air. ClaimStack gives every section ~1000px
   of height for 100-180 words; ours packed 220 words into 500px. Centered
   headlines at 44-56px, one visual per section, and a light ground. Word
   count was never the problem (theirs is higher); density and the absence of
   anything to look at was. The founder called the old page an encyclopedia.
   =========================================================================== */

.mkt {
  padding: 5rem 0 4.5rem;
  text-align: center;
}
.mkt.sunk {
  background: var(--card-sunk);
  border-top: 1px solid var(--line-soft);
  border-bottom: 1px solid var(--line-soft);
  margin: 0 calc(50% - 50vw);
  padding-left: calc(50vw - 50%);
  padding-right: calc(50vw - 50%);
}
/* The full-bleed trick failed once (content crushed into a word-per-line
   column). The difference this time: no percentage maths on the CHILDREN -
   every child is centred with a max-width and auto margins, so the bleed only
   ever touches the background. Verified at 390 / 768 / 1440 after it went in. */
.mkt > * { max-width: 62rem; margin-left: auto; margin-right: auto; }
.mkt h2 {
  font-size: var(--fs-4xl);
  line-height: 1.1;
  letter-spacing: -0.03em;
  max-width: 24ch;
  margin: 0 auto 1rem;
  text-wrap: balance;
}
.eyebrow.center { margin-bottom: 1rem; }
.center { text-align: center; }
.lede-center {
  font-size: var(--fs-md);
  color: var(--ink-soft);
  max-width: 42rem;
  margin: 0 auto 1.75rem;
}
.mkt .primary { margin-top: 0.5rem; }

.trio, .steps3, .docs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  gap: 2rem;
  margin-top: 2.75rem;
  text-align: left;
  list-style: none;
  padding: 0;
}
.trio svg, .quad svg {
  width: 28px; height: 28px;
  stroke: var(--accent); fill: none; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
  margin-bottom: 0.9rem;
}
.pain h3, .steps3 h3, .point h3 { margin: 0 0 0.4rem; font-size: var(--fs-lg); }
.pain p, .steps3 p, .point p, .doc span { margin: 0; color: var(--ink-soft); }

.steps3 li {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.75rem 1.5rem 1.6rem;
  box-shadow: var(--shadow);
}
.steps3 .num {
  display: block;
  font-family: var(--mono);
  font-size: var(--fs-xl);
  color: var(--accent-ink);
  margin-bottom: 0.8rem;
}

.quad {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  gap: 2rem 3rem;
  margin-top: 2.75rem;
  text-align: left;
}

/* Document thumbnails drawn in CSS: a sheet with a few lines on it. */
.doc { margin: 0; text-align: left; }
.paper {
  aspect-ratio: 8.5 / 11;
  max-width: 11rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 6px;
  box-shadow: var(--shadow-lift);
  padding: 1.1rem 1rem;
  display: flex; flex-direction: column; gap: 0.55rem;
  margin-bottom: 1rem;
}
.paper i { display: block; height: 6px; border-radius: 3px; background: var(--line); }
.paper i:first-child { width: 55%; height: 9px; background: var(--accent-soft); }
.paper i.short { width: 60%; }
.paper.rows i { height: 5px; }
.paper.rows i:nth-child(odd) { background: var(--line-soft); }
.paper.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
.paper.grid i { height: auto; aspect-ratio: 4 / 3; border-radius: 4px; background: var(--accent-soft); width: auto; }
.doc figcaption strong { display: block; margin-bottom: 0.3rem; font-size: var(--fs-lg); }

/* FAQ accordion - native details, no JS, keyboard-accessible by default. */
.faq-list { text-align: left; max-width: 44rem; margin: 2.25rem auto 1.25rem; }
.faq-item { border-top: 1px solid var(--line); }
.faq-item:last-child { border-bottom: 1px solid var(--line); }
.faq-item summary {
  cursor: pointer;
  list-style: none;
  padding: 1.1rem 2.25rem 1.1rem 0;
  font-weight: 650;
  font-size: var(--fs-md);
  position: relative;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: "+";
  position: absolute; right: 0.25rem; top: 50%;
  transform: translateY(-50%);
  color: var(--accent-ink);
  font-size: var(--fs-xl);
  font-weight: 400;
}
.faq-item[open] summary::after { content: "\2013"; }
.faq-item p { margin: 0 0 1.2rem; color: var(--ink-soft); max-width: 60ch; }

.closing-mkt { padding-bottom: 5.5rem; }
.hero-actions.center { justify-content: center; }

/* ---------------------------------------------------------------------------
   THE APP WINDOW in the hero: the review screen itself - window bar, a count,
   three rows with code / name / quantity, the reason, a photo chip and the two
   buttons. A lone text card beside the headline read as arbitrary; a product
   window beside a product headline is the pattern every competitor uses.
   --------------------------------------------------------------------------- */
.app-window {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: 0 18px 50px rgba(16, 21, 28, 0.14), var(--shadow);
  overflow: hidden;
  text-align: left;
  font-size: var(--fs-sm);
}
.app-bar {
  display: flex; align-items: center; gap: 0.45rem;
  padding: 0.6rem 0.9rem;
  background: var(--card-sunk);
  border-bottom: 1px solid var(--line);
}
.app-bar span { width: 10px; height: 10px; border-radius: 50%; background: var(--line); }
.app-bar em {
  margin-left: 0.6rem; font-style: normal; color: var(--muted);
  font-size: var(--fs-xs); letter-spacing: 0.02em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.app-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  padding: 0.9rem 1rem 0.6rem;
}
.app-head strong { font-size: var(--fs-base); }
.app-rows { list-style: none; margin: 0; padding: 0 1rem 1rem; }
.app-rows li { padding: 0.8rem 0; border-top: 1px solid var(--line-soft); }
.row-main { display: flex; align-items: baseline; gap: 0.7rem; }
.row-code {
  font-family: var(--mono); font-size: var(--fs-xs);
  color: var(--accent-ink); background: var(--accent-soft);
  padding: 0.1rem 0.4rem; border-radius: 4px; white-space: nowrap;
}
.row-code.blank { color: var(--muted); background: var(--line-soft); min-width: 2.2rem; text-align: center; }
.row-name { font-weight: 600; color: var(--ink); flex: 1; }
.row-qty { font-family: var(--mono); color: var(--ink-soft); white-space: nowrap; }
.row-why { margin: 0.35rem 0 0.55rem; color: var(--ink-soft); }
.row-foot { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
/* The chip is shared with the approval card (see REVIEW CARD at the end of this
   file): the landing page draws a mock of that card, and the two must not drift
   into two different pills. Only the layout is shared here - the fill is set by
   the variants below and by the card's own rules. */
.chip {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-1);
  max-width: 100%;
  font-size: var(--fs-xs); font-weight: 650; padding: 0.2rem 0.55rem; border-radius: 999px;
}
.chip.ok { background: var(--ok-soft); color: var(--ok-ink); }
.chip.warn { background: var(--warn-bg); color: var(--warn-ink); }
.btn-mock {
  margin-left: auto; font-size: var(--fs-xs); font-weight: 650;
  padding: 0.3rem 0.7rem; border-radius: var(--radius-sm);
  background: var(--accent); color: #fff;
}
.btn-mock.ghost { margin-left: 0; background: transparent; color: var(--ink-soft); border: 1px solid var(--line); }

@media (max-width: 760px) {
  .mkt { padding: 3.5rem 0 3rem; }
  .app-bar em { display: none; }
  .btn-mock { margin-left: 0; }
}

/* Phone order: copy first, window second. Measured 2026-08-21 on the live
   page at 390px: the grid collapsed with the window ABOVE the headline -
   a visitor saw a product screen before being told what the product was.
   An older rule promoted the illustration on phones; this wins by cascade. */
@media (max-width: 760px) {
  .hero { display: flex; flex-direction: column; }
  .hero-copy { order: 0; }
  .app-window, .hero-art { order: 1; margin-top: 1.5rem; }
}
.app-head strong { white-space: nowrap; }
.mkt .post-teasers { text-align: left; }


/* "Continue with Google" - white button, official four-colour G, per Google's
   sign-in branding rules (no recolouring, clear space around the mark). */
.google-button {
  display: flex; align-items: center; justify-content: center; gap: 0.65rem;
  width: 100%;
  padding: 0.62rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card);
  color: var(--ink);
  font-weight: 600;
  text-decoration: none;
  box-shadow: var(--shadow);
}
.google-button:hover { border-color: var(--muted); text-decoration: none; background: var(--card-sunk); }
.auth-divider {
  display: flex; align-items: center; gap: 0.75rem;
  margin: 1.1rem 0;
  font-size: var(--fs-sm);
}
.auth-divider::before, .auth-divider::after { content: ""; flex: 1; border-top: 1px solid var(--line-soft); }


/* ===========================================================================
   SIGNED-IN HOME (21 Aug 2026). The first screen after sign-in was two boxes
   of plain text and "No jobs yet." - the founder signed in with Google and
   said: no sign, no symbol, no picture anywhere. Two states, two shapes.
   =========================================================================== */

/* Empty state: one centred card, a mark, three steps, one big button. */
.home-empty {
  max-width: 44rem;
  margin: 2.5rem auto;
  padding: 3rem 2rem 2.5rem;
  text-align: center;
}
.home-empty h1 { font-size: var(--fs-2xl); margin: 0.5rem 0 0.4rem; letter-spacing: -0.02em; }
.home-mark { width: 56px; height: 56px; color: var(--accent); display: block; margin: 0 auto 0.5rem; }
.steps-mini {
  list-style: none; padding: 0;
  margin: 2rem 0 1.75rem;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
  gap: 1.5rem 1.75rem;
  text-align: left;
}
.steps-mini svg {
  width: 26px; height: 26px; stroke: var(--accent); fill: none;
  stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
  display: block; margin-bottom: 0.6rem;
}
.steps-mini strong { display: block; margin-bottom: 0.25rem; }
.steps-mini span { color: var(--ink-soft); font-size: var(--fs-sm); }
.primary.big { font-size: var(--fs-md); padding: 0.85rem 1.75rem; }
.home-empty .charge-free { margin-top: 1rem; }

/* With jobs: a short start row, then cards. */
.home-start {
  display: flex; align-items: center; justify-content: space-between; gap: 1.5rem;
  flex-wrap: wrap;
}
.home-start h1 { font-size: var(--fs-xl); margin: 0 0 0.3rem; }
.home-start p { margin: 0.2rem 0 0; max-width: 44rem; }

.home-jobs h2 { font-size: var(--fs-lg); margin: 1.5rem 0 0.75rem; }
.jobcards { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.75rem; }
.jobcard {
  display: grid;
  /* main | pill | time | action | delete. The fifth column was added with the
     Delete link on 21 Aug; it belongs HERE and not in a later rule, because a
     later `.jobcard {}` of equal specificity would also win over the mobile
     media query below and quietly put five columns on a 390px screen. */
  grid-template-columns: 1fr auto auto auto auto;
  align-items: center;
  gap: 1rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-left: 4px solid var(--line);
  border-radius: var(--radius);
  padding: 0.9rem 1.1rem;
  box-shadow: var(--shadow);
}
.jobcard.status-awaiting_approval { border-left-color: var(--warn-line); }
.jobcard.status-failed { border-left-color: var(--error); }
.jobcard.status-done { border-left-color: var(--ok); }
.jobcard-main { display: flex; flex-direction: column; min-width: 0; }
.jobcard-title { font-weight: 650; text-decoration: none; color: var(--ink); }
.jobcard-title:hover { color: var(--accent-ink); }
.jobcard-time { font-size: var(--fs-sm); white-space: nowrap; }
.jobcard-action { white-space: nowrap; }
@media (max-width: 640px) {
  .jobcard { grid-template-columns: 1fr auto; }
  .jobcard-time { display: none; }
  .jobcard .pill { justify-self: end; }
  .jobcard-action { grid-column: 1 / -1; justify-self: start; }
}


/* ===========================================================================
   JOB FACTS FORM, SECOND PASS (21 Aug 2026). 48 text boxes in one column with
   no explanation read as "five hundred rows" to the founder on his first real
   job. Now: essentials alone at the top, a two-column grid, native pickers,
   collapsed advanced sections, and an (i) beside every field.
   =========================================================================== */

.facts-section {
  border: 0;
  padding: 0;
  margin: 0 0 2rem;
}
.facts-section h3 { margin: 0 0 0.25rem; font-size: var(--fs-lg); }
.facts-section > .muted { margin: 0 0 1rem; max-width: 60ch; }
.facts-section h4 { margin: 1.5rem 0 0.25rem; font-size: var(--fs-base); }

.facts-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
  gap: 0.9rem 1.5rem;
}
.field { display: block; margin: 0; }
.field-label {
  display: flex; align-items: center; gap: 0.35rem;
  font-size: var(--fs-sm);
  font-weight: 550;
  color: var(--ink);
  margin-bottom: 0.3rem;
}
.field > input, .field > select { display: block; width: 100%; margin: 0; }

/* Folded sections (.step-fold) are styled in the third-pass block below. */

.threshold {
  margin-top: 1.25rem;
  padding: 1rem 1.1rem;
  border: 1px solid var(--warn-line);
  border-radius: var(--radius);
  background: var(--warn-bg);
}
.threshold .muted { margin: 0.6rem 0 0; font-size: var(--fs-sm); }

.facts-actions { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-top: 0.5rem; }
.facts-actions .muted { font-size: var(--fs-sm); max-width: 40ch; }

/* The (i). A real button (keyboard reachable, tap-to-open on a phone through
   focus); the bubble is pure CSS - no script, no request. */
.info { position: relative; display: inline-flex; }
.info-btn {
  width: 1.15rem; height: 1.15rem; padding: 0;
  border-radius: 50%;
  border: 1px solid var(--muted);
  background: transparent;
  color: var(--muted);
  font: 650 var(--fs-xs)/1 var(--mono);
  cursor: help;
  display: inline-grid; place-items: center;
}
.info-btn:hover, .info-btn:focus { border-color: var(--accent); color: var(--accent-ink); }
.info-tip {
  display: none;
  position: absolute; left: 50%; bottom: calc(100% + 0.5rem);
  transform: translateX(-50%);
  width: 18rem; max-width: 80vw;
  padding: 0.6rem 0.75rem;
  background: var(--ink);
  color: var(--bg);
  font-size: var(--fs-sm); font-weight: 400; line-height: 1.45;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lift);
  z-index: 30;
}
.info:hover .info-tip, .info:focus-within .info-tip { display: block; }
.info-tip::after {
  content: ""; position: absolute; top: 100%; left: 50%;
  margin-left: -6px; border: 6px solid transparent; border-top-color: var(--ink);
}
@media (max-width: 640px) {
  .info-tip { left: auto; right: -0.5rem; transform: none; }
  .info-tip::after { left: auto; right: 0.7rem; margin-left: 0; }
}

/* ===========================================================================
   UPLOAD BOXES, THIRD PASS (21 Aug 2026). The browser's native file control is
   hidden: it labels itself in the browser's own language (a Turkish Chrome
   writes both the button and the "no file chosen" text in Turkish) and the
   customer would see a foreign line on an English screen. Our own button opens
   the same picker. Picking sends
   (intake.js); the Add button is the no-JavaScript fallback only.
   =========================================================================== */
.uploadforms {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  gap: 1rem;
  margin-top: 0.75rem;
}
.uploadform.dropzone {
  margin: 0;
  padding: 1rem 1.1rem;
  border: 1.5px dashed var(--line);
  border-radius: var(--radius);
  background: var(--card-sunk);
  transition: border-color 150ms ease-out, background-color 150ms ease-out;
}
.uploadform.dropzone.dragging { border-color: var(--accent); background: var(--accent-soft); }
.upload-title { display: block; font-weight: 600; font-size: var(--fs-sm); color: var(--ink); }
.uploadform .limits { margin: 0.15rem 0 0.7rem; font-size: var(--fs-xs); }
.pickrow { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.file-pick {
  display: inline-flex; align-items: center;
  padding: 0.5rem 1rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: #fff;
  font-weight: 600; font-size: var(--fs-sm);
  cursor: pointer;
  white-space: nowrap;
}
.file-pick:hover { background: var(--accent-strong); border-color: var(--accent-strong); }
/* Keyboard focus lives on the hidden input; show it on the visible button. */
.dropzone:has(input[type="file"]:focus-visible) .file-pick,
.dropzone:has(input[type="file"]:focus-visible) .camera-button {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.uploadform .drophint { margin: 0; font-size: var(--fs-xs); }
.uploadform .chosen { margin: 0.5rem 0 0; font-size: var(--fs-sm); }
.zone-notice:empty { display: none; }
.zone-notice .error { margin: 0.5rem 0 0; }
/* The Add button exists for browsers without scripts. With scripts, choosing a
   file sends it, and a button that does nothing visible is worse than none. */
html.js .add-fallback { display: none; }
/* The camera button opens a file picker on a desktop - a surprise, not a
   feature. Devices with a fine pointer that can hover do not get it. */
@media (hover: hover) and (pointer: fine) {
  .camera-button { display: none; }
}

/* ===========================================================================
   JOB FACTS FORM, THIRD PASS (21 Aug 2026). Numbered steps with a tinted
   header band; folded sections share the band as their summary. The blue is
   the existing accent, not a new colour.
   =========================================================================== */
.facts-section.step { margin-bottom: 1.5rem; }
.step-head, .step-fold > summary {
  display: flex; align-items: center; gap: 0.65rem; flex-wrap: wrap;
  margin: 0 0 1rem;
  padding: 0.6rem 0.85rem;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
}
.step-head h3, .step-fold > summary h3 {
  margin: 0;
  font-size: var(--fs-base);
  font-weight: 650;
  color: var(--accent-strong);
}
.step-no {
  flex: 0 0 auto;
  width: 1.65rem; height: 1.65rem;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  font: 650 var(--fs-sm)/1 var(--font);
  display: inline-grid; place-items: center;
}
.step-optional .step-no {
  background: transparent;
  color: var(--accent-ink);
  border: 1.5px dashed var(--accent);
}
.step-fold {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  padding: 0;
}
.step-fold > summary {
  cursor: pointer;
  list-style: none;
  margin: 0;
  border-radius: var(--radius);
  position: relative;
  padding-right: 2.25rem;
}
.step-fold > summary::-webkit-details-marker { display: none; }
.step-fold > summary::after {
  content: "";
  position: absolute; right: 1rem; top: 50%;
  width: 0.5em; height: 0.5em;
  margin-top: -0.45em;
  border-right: 2px solid var(--accent-ink); border-bottom: 2px solid var(--accent-ink);
  transform: rotate(45deg);
  transition: transform 200ms ease-out;
}
.step-fold[open] > summary { border-radius: var(--radius) var(--radius) 0 0; }
.step-fold[open] > summary::after { transform: rotate(-135deg); margin-top: -0.15em; }
.fold-note { font-size: var(--fs-sm); flex: 1 1 14rem; }
.fold-note strong { color: var(--ink); font-weight: 600; }
.fold-body { padding: 1rem 1.1rem 1.2rem; }
.fold-body h4 { margin: 0 0 0.6rem; font-size: var(--fs-sm); font-weight: 650; color: var(--accent-strong); letter-spacing: 0.01em; text-transform: uppercase; }
.fold-body .facts-grid + h4, .fold-body .elevations + h4 { margin-top: 1.4rem; }
.facts-grid.cols-3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); }
.threshold { margin-top: 1rem; padding: 0.8rem 1rem; }
.threshold .field { max-width: 36rem; }
.pickrow .camera-button { font-size: var(--fs-sm); padding: 0.5rem 0.9rem; }
/* The generic input rule (padding, min-height) outranks .visually-hidden by
   specificity; the hidden file inputs must not keep a 44px box. */
.dropzone input[type="file"].visually-hidden { width: 1px; height: 1px; min-height: 0; padding: 0; border: 0; }


/* ===========================================================================
   TAKING THINGS BACK (21 Aug 2026): removing one uploaded file, and deleting
   a whole job.

   No new colour is introduced. The red is --error, which both palettes already
   define; a hard-coded red would have gone invisible the moment the reader's
   phone was in dark mode, which is the same defect the link colour had.

   The destructive control is deliberately the quietest thing on the page until
   it is opened: an outlined button, never a filled one. A filled red button at
   the foot of the job page competes with the primary action above it, and the
   one action nobody should hit by accident should not be the loudest.
   =========================================================================== */

/* --- one file: the Remove button in the uploaded-files list --------------- */
/* A refused Remove lands here, inside the Files card, instead of in the page
   notice at the very top. Empty by default and empty again after every
   successful redraw, so a stale message never sits above a fresh list. */
.uploads-notice:empty { display: none; }
.uploads-notice { margin: 0 0 0.75rem; }
.uploads-notice .error { margin: 0; }
.uploadlist li .removeform { margin: 0 0 0 auto; }
.remove-button {
  font-size: var(--fs-sm);
  font-weight: 500;
  padding: 0.2rem 0.6rem;
  color: var(--muted);
  background: transparent;
  border: 1px solid var(--line);
}
.remove-button:hover:not([disabled]) {
  color: var(--error);
  border-color: var(--error);
  background: var(--card);
}

/* --- the whole job ------------------------------------------------------- */
.card.danger { border-color: var(--line); }
.card.danger h2 { color: var(--ink-soft); }
.delete-fold { margin-top: 0.75rem; }
/* The first step only opens the confirm block. It is styled as a secondary
   button because that is what it is: nothing is deleted by pressing it. */
.danger-reveal {
  display: inline-block;
  cursor: pointer;
  list-style: none;
  font-weight: 550;
  padding: 0.52rem 0.95rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card);
  color: var(--ink);
}
.danger-reveal::-webkit-details-marker { display: none; }
.danger-reveal:hover { border-color: var(--error); color: var(--error); }
.delete-confirm {
  margin-top: 1rem;
  padding: 1rem 1.1rem;
  border: 1px solid var(--error);
  border-radius: var(--radius);
  background: var(--card-sunk);
  max-width: var(--measure);
}
.delete-confirm p { margin: 0 0 0.6rem; }
.delete-form { margin: 0.9rem 0 0.6rem; }
.danger-button {
  color: var(--error);
  background: var(--card);
  border: 1px solid var(--error);
  font-weight: 600;
  padding: 0.58rem 1.15rem;
}
/* Filled only on hover/press, so the shape the reader commits to is the one
   they are already pointing at. --card as the text colour flips with the
   theme: dark ink on the light-red of dark mode, white on the deep red of
   light mode. */
.danger-button:hover:not([disabled]),
.danger-button:active:not([disabled]) {
  background: var(--error);
  border-color: var(--error);
  color: var(--card);
}

/* --- the quiet Delete link on a dashboard card ---------------------------
   The card's fifth grid column lives with the rest of the .jobcard rule
   further up, deliberately: repeating `.jobcard {}` down here would tie on
   specificity with the mobile media query above it and, being later, win —
   five columns on a phone. */
.jobcard-delete { font-size: var(--fs-sm); color: var(--muted); white-space: nowrap; }
.jobcard-delete:hover { color: var(--error); }
@media (max-width: 640px) {
  /* At 390px the card is two columns; the primary action already takes a full
     row of its own, and Delete takes the next one under it. Last on the card,
     which is where the one irreversible action belongs. */
  .jobcard-delete { grid-column: 1 / -1; justify-self: start; }
}
.deleted-line { margin: 0 0 1rem; }

/* ===========================================================================
   COMPANY LOGO (21 Aug 2026). The contractor's own letterhead, printed at the
   top of the supplement, the photo log and the homeowner summary. Never the
   product's logo - the documents belong to the contractor.

   The block sits inside step 3 of the job facts form, under the company
   fields, and carries its own two small forms: HTML forbids a form inside a
   form, so the facts form element closes after step 2 and step 3's fields are
   tied to it with form="facts-form" (measured in Chrome, 22 Aug 2026). No new
   colour, no new control: the picker reuses .file-pick and the quiet Remove
   reuses .remove-button.
   =========================================================================== */
/* The logo block sits inside step 3's fold body under an h4 (22 Aug 2026): the
   facts form element closes after step 2 and step 3's fields are tied to it
   with form="facts-form", so the logo's own forms are siblings, not nested. */
#logo-block { margin-top: 0.25rem; }
.logo-controls { display: flex; flex-direction: column; align-items: flex-start; gap: 0.7rem; }
.logo-controls .field-label { display: inline-flex; align-items: center; gap: 0.35rem; }
.logoform.dropzone {
  margin: 0;
  align-self: stretch;
  padding: 0.85rem 1rem;
  border: 1.5px dashed var(--line);
  border-radius: var(--radius);
  background: var(--card-sunk);
  transition: border-color 150ms ease-out, background-color 150ms ease-out;
}
.logoform.dropzone.dragging { border-color: var(--accent); background: var(--accent-soft); }
.logoform .chosen { margin: 0.5rem 0 0; font-size: var(--fs-sm); }
/* Printed at 150px wide; on screen it only has to prove which file is stored,
   so it is capped rather than scaled up. The checked background shows where a
   transparent PNG really ends. */
.logo-thumb {
  max-height: 3rem;
  max-width: 100%;
  width: auto;
  padding: 0.3rem 0.4rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card);
}
.logo-removeform { margin: 0; }

/* ===========================================================================
   SUPPLEMENT OUTCOME (22 Aug 2026). The contractor's own bookkeeping, at the
   foot of the run card under the download links: when it was sent, what the
   carrier answered, how much was approved. None of it is printed on anything
   or sent anywhere, and the screen never turns it into a ratio.

   No new visual language: the fields reuse .facts-grid / .field /
   .field-label / .info from the job facts form, and the tally on the
   dashboard is one muted line. The only new rule here is the divider that
   separates "here are your files" from "what happened after you sent them".
   =========================================================================== */
.outcome {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}
.outcome h3 { margin: 0 0 0.25rem; font-size: var(--fs-lg); }
.outcome > .muted { margin: 0 0 1rem; max-width: 60ch; }
.outcome .facts-actions { margin-top: 1rem; }
/* The dashboard tally. One line, above the cards, only when something has
   been recorded. */
.outcome-tally { margin: 0 0 0.75rem; font-size: var(--fs-sm); }
/* An outcome pill carries the carrier's answer instead of the job status.
   Same shape as every other pill; only the wording changes, so the eye does
   not learn a second badge. */
.pill-outcome { border-color: var(--accent); color: var(--accent-ink); }

/* ===========================================================================
   REVEALED FOLLOW-UP FIELDS (22 Aug 2026). A wrapper with data-reveal-when is
   hidden by facts.js while its parent answer is not the one that makes it
   meaningful. `.field` sets display:block, which would outrank the browser's
   own [hidden] rule, so the hidden state is written out explicitly.
   =========================================================================== */
.field-reveal[hidden] { display: none; }
.field-reveal .field { border-left: 3px solid var(--accent-soft); padding-left: 0.7rem; }

/* ===========================================================================
   JOB STEP BAR (22 Aug 2026). The job page listed cards - status, Files, Job
   facts, Photo plan, Review, Delete - and never said the order of the work or
   where the finished package would appear. Four steps, in order, with the one
   that is running marked.

   No second visual language: the numbered disc is the .step-no already used by
   the job facts form, and every colour is an existing token. Only the row
   layout, the connector line and the three states are new here.

   The bar is drawn inside the run card, so a job that is being read redraws it
   with every poll instead of leaving a stale position on the screen.
   =========================================================================== */
.jobsteps {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3) 0;
  list-style: none;
  margin: 0 0 var(--sp-5);
  padding: 0;
}
.jobstep {
  flex: 1 1 8rem;
  min-width: 8rem;
  position: relative;
  text-align: center;
}
/* The connector runs from the previous disc to this one, so it never hangs off
   the left edge of the first step. It sits behind the discs. */
.jobstep + .jobstep::before {
  content: "";
  position: absolute;
  /* Half the height of a .step-no disc, so the line meets its centre. */
  top: 0.825rem;
  right: 50%;
  left: -50%;
  border-top: 2px solid var(--line);
  z-index: 0;
}
.jobstep-link {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-1);
  padding: 0 var(--sp-1);
  font-size: var(--fs-sm);
  color: var(--muted);
  text-decoration: none;
}
a.jobstep-link:hover .jobstep-label { text-decoration: underline; }
/* The default disc is filled accent; a step that has not happened yet must not
   look finished, so the fill is taken away and given back per state. */
.jobstep .step-no {
  background: var(--card);
  border: 2px solid var(--line);
  color: var(--muted);
}
.jobstep.is-done .step-no {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.jobstep.is-current .step-no {
  border-color: var(--accent);
  color: var(--accent-ink);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.jobstep.is-blocked .step-no {
  border-color: var(--error);
  color: var(--error);
}
.jobstep.is-done .jobstep-link,
.jobstep.is-current .jobstep-link { color: var(--ink); }
.jobstep.is-current .jobstep-label { font-weight: 650; }
.jobstep.is-blocked .jobstep-link { color: var(--error); }
/* "You are here" - a SECOND axis, not a fourth state (23 Aug 2026, measured).
   The three states above say where the JOB is. This one says which step is on
   the screen right now, and the two are different most of the time: a job
   waiting for approval sits on step 3 while the contractor walks back to step 1
   to swap a photograph. Without this the bar rendered byte for byte the same on
   all four steps - press Next, the body changes, the navigation does not move.

   Marked on the LABEL, not on the disc: the disc already carries the whole
   progress vocabulary (filled / ringed / error), and a fourth disc treatment
   would be read as a fourth progress state. An underline under the label is the
   ordinary "current tab" idiom and cannot be confused with progress.

   box-shadow and not border-bottom: a border adds 2px of height to one item
   only and pushes its label out of line with the other three. */
.jobstep.is-viewing .jobstep-link { color: var(--ink); }
.jobstep.is-viewing .jobstep-label {
  font-weight: 650;
  box-shadow: 0 2px 0 var(--accent);
}
/* One line, under the running step only. --fs-sm and not --fs-xs: this is a
   sentence, and the extra-small token is reserved for micro-labels. */
.jobstep-note {
  display: block;
  margin-top: var(--sp-1);
  padding: 0 var(--sp-1);
  font-size: var(--fs-sm);
  color: var(--muted);
}
/* Under about 480px four steps in a row leave no room for the labels, so they
   wrap two by two. The connector is dropped there: a line that crosses a wrap
   points at the wrong disc. */
@media (max-width: 30rem) {
  .jobstep { flex: 1 1 45%; min-width: 45%; }
  .jobstep + .jobstep::before { display: none; }
}

/* The photo checklist is one line, and it now sits under the photo box rather
   than at the foot of the Files card: nobody opens this application on the
   roof, so the plan is a printout for the crew, and the question it answers
   ("where do I get these frames?") is asked at the photo box. */
.prep-line {
  margin: var(--sp-3) 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: baseline;
}
.prep-note { margin: 0; max-width: var(--measure); font-size: var(--fs-sm); }

/* ---------------------------------------------------------------------------
   Job page, wave 2 (23 Aug 2026)

   Four measured complaints from the founder's first real job, in order:

   1. "there is no telling that it uploaded ... there is no animation."
      The bar was not missing. It was three pixels tall. Two rules claimed the
      same class name and, with border-box sizing, cancelled each other out:

        .progress { height: 1.1rem; overflow: hidden; }        (upload box)
        .progress { padding-top: .85rem; border-top: 1px; }    (run card)

      17.6px - 13.6px - 1px = 3px of drawable height, under a border line that
      read as a section rule. The upload bar now carries its own class and
      undoes the run-card block treatment.

   2. "200 photos, will there be 200 rows?" — one summary row, a folded grid.
   3. "why do I go to the top of the page when I click something down here?" —
      refusals land in the block that owns the button.
   4. "the new job screen is confusing, there is a log and everything" — the
      numbers and the log fold away until something has actually run.

   No new colour and no new shape: the grid borrows the card-sunk surface, the
   remove control is the .remove-button already used in the file list, and the
   folds are the same <details> the delete confirmation uses.
   --------------------------------------------------------------------------- */

/* 1 — the upload bar is its own thing, not a section of the run card. */
.upload-progress {
  margin: var(--sp-2) 0 0;
  padding-top: 0;
  border-top: 0;
  height: 0.9rem;
}
/* The box says it is working: the picker stops answering the mouse while a
   file is on the wire. Nothing is disabled in the DOM — see intake.js. */
.uploadform.sending { border-style: solid; border-color: var(--accent); }
.uploadform.sending .file-pick,
.uploadform.sending .camera-button,
.uploadform.sending .add-fallback {
  pointer-events: none;
  opacity: 0.55;
}
.uploadform.sending .drophint { visibility: hidden; }
.upload-added { margin: var(--sp-2) 0 0; font-size: var(--fs-sm); }

/* A box is the form plus that form's own answer. The wrapper exists because a
   form may not contain another form, and the per-photo Remove controls are
   forms. */
.uploadbox + .uploadbox { margin-top: var(--sp-3); }

/* 2 — photographs: one summary row, a folded grid of thumbnails. */
.photo-files { margin-top: var(--sp-2); }
.photo-summary {
  cursor: pointer;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: baseline;
  padding: var(--sp-2) 0;
}
.photo-summary-count { font-weight: 600; }
.photo-summary-open { font-size: var(--fs-sm); }
.photo-grid {
  list-style: none;
  padding: 0;
  margin: var(--sp-2) 0 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
  gap: var(--sp-3);
}
.photo-cell {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  align-items: flex-start;
  background: var(--card-sunk);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--sp-2);
}
.photo-thumb {
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--line-soft);
}
/* The file name is the only way to tell two frames apart, so it is kept, but
   it may not push the cell wider than the grid track. */
.photo-name {
  font-size: var(--fs-xs);
  color: var(--muted);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.photo-grid-capped { margin: var(--sp-3) 0 0; font-size: var(--fs-sm); }

/* 3 — a refusal belongs beside the control that was pressed. The slot is empty
   markup until a refusal lands in it, so it collapses; the line naming the fix
   appears with it and only with it. No script decides this. */
.form-notice:empty { display: none; }
.form-notice { margin: var(--sp-2) 0 0; }
.form-notice .error { margin: 0; }
.notice-fix { display: none; margin: var(--sp-1) 0 0; font-size: var(--fs-sm); }
.form-notice:not(:empty) + .notice-fix { display: block; }

/* 4 — a job that has not run yet folds its numbers and its log away. Nothing
   is removed: the fold is one click, and the summary says what is inside. */
.job-quiet { margin: var(--sp-3) 0; }
.job-quiet > summary {
  cursor: pointer;
  color: var(--muted);
  font-size: var(--fs-sm);
}
.job-quiet .facts { margin-top: var(--sp-2); }

/* ---------------------------------------------------------------------------
   Account page

   No new colour, no new shape. The header facts reuse the two-column grid the
   job card already uses for its facts list; that rule is scoped under .status,
   so it is repeated here rather than widened — widening it would restyle a
   card nobody asked us to touch. The forms borrow the label treatment of the
   sign-in card for the same reason a password field looks the same on both
   screens: it is the same kind of object.

   The close-the-account section reuses .card.danger, .danger-reveal,
   .delete-confirm and .danger-button unchanged.
   --------------------------------------------------------------------------- */
.account-facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.15rem 0.75rem;
  font-size: var(--fs-base);
}
.account-facts dt { color: var(--muted); }
.account-facts dd { margin: 0; }

.account-form label {
  display: block;
  margin-bottom: 1rem;
  font-weight: 550;
  font-size: var(--fs-sm);
}
.account-form label > input { margin-top: 0.35rem; font-weight: 400; }
.account-form small { display: block; margin-top: 0.4rem; font-weight: 400; }
/* A checkbox and its sentence sit on one line; the sentence is body text, so
   it drops the label weight the typed fields carry. */
.account-form label.check {
  display: flex;
  gap: 0.4rem;
  align-items: center;
  font-weight: 400;
  font-size: var(--fs-base);
}
.account-form label.check > input { margin-top: 0; }

/* ---------------------------------------------------------------------------
   Job page, wave 3 (23 Aug 2026): one step at a time

   Three measured complaints from the founder, running his own job:

   1. "why should he see the file price, the size ... there is a lot that does
      not concern him" — the run-cost card, per-file byte counts and the
      megabyte total are off the screen. The numbers are not lost: cost is on
      the record and in the run log, the per-file limit still sits above the
      upload box where it prevents a failed upload.
   2. "it already shows 4 steps up there ... it should go next, next, section
      by section" — the page draws one step and the bar navigates.
   3. "while it writes the file there should be a filling symbol" — the run
      block carries a real <progress> element.

   No new colour and no new shape: the nav borrows the link colour already in
   the palette, and the meter is the browser's own element tinted with
   --accent.
   --------------------------------------------------------------------------- */

/* The two step links sit under the step body, apart from the card stack: back
   on the left, forward on the right, so the forward one lands where a thumb
   already is on a phone. */
.stepnav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  align-items: baseline;
  justify-content: space-between;
  margin: var(--sp-4) 0 var(--sp-3);
}
/* Back is quiet, forward is not: the forward link is the one being looked for.
   Neither is a filled button — the card above owns the primary action. */
.stepnav-back { font-size: var(--fs-sm); }
.stepnav-next { font-weight: 600; margin-left: auto; }

/* The filling bar. Determinate when the engine reported a step count,
   indeterminate when it did not; the browser draws both, so no script and no
   invented percentage. Cross-browser tinting needs all three selectors. */
.run-meter {
  display: block;
  width: 100%;
  height: 0.55rem;
  margin: 0 0 var(--sp-2);
  border: 0;
  border-radius: 999px;
  overflow: hidden;
  background: var(--accent-soft);
  color: var(--accent);
  appearance: none;
  -webkit-appearance: none;
}
.run-meter::-webkit-progress-bar { background: var(--accent-soft); }
.run-meter::-webkit-progress-value { background: var(--accent); }
.run-meter::-moz-progress-bar { background: var(--accent); }

/* A pressed button says what it is doing. The sentence rides beside the button
   and takes no room until the request starts.

   Measured 23 Aug 2026: htmx puts .htmx-request on the element that ISSUES the
   request, or on whatever hx-indicator names - never on a span inside it. These
   two sentences sit inside a <form hx-post> that names no indicator, so the
   form is the element that gets marked and the rule below must be a DESCENDANT
   rule. The single-element rule was there first and showed the sentence never:
   markup that looked right and did nothing. Both forms are kept, so the
   sentence also works if a form ever points hx-indicator straight at it. */
.run-busy {
  margin-left: var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--muted);
}
.run-busy.htmx-indicator { display: none; }
.htmx-request .run-busy.htmx-indicator,
.run-busy.htmx-indicator.htmx-request { display: inline; }

/* ---------------------------------------------------------------------------
   Package step, wave 4 (25 Aug 2026): the result, then the files

   Measured, watching the finished product: "when the file is finally produced
   the system turns into a LIBRARY - it just hands the guy a list of 3-4 files.
   The site looks too texty." Step 4 is the reward moment and it opened with
   five filenames, a five-column table, a form and eight paragraphs.

   Nothing was removed. The heavy blocks fold into <details>, which needs no
   script and no ARIA, and the two blocks that must never be confused - what
   goes to the carrier and what must not - stay unfolded and apart.

   No new colour and no new size: every value below comes from the palette and
   the type scale that were already here.
   --------------------------------------------------------------------------- */

/* The band that says "done, and here is what you got". A tinted card rather
   than another run of body text, so the eye lands on it first. */
.pack-result {
  margin: 0 0 var(--sp-4);
  padding: var(--sp-4);
  border: 1px solid var(--line);
  border-left: 3px solid var(--ok);
  border-radius: var(--radius);
  background: var(--card-sunk);
}
.pack-result h3 { margin: 0 0 var(--sp-1); }
.pack-meta {
  margin: 0 0 var(--sp-3);
  color: var(--muted);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

/* The items that went in, as chips: a name and a measure, never a price. The
   micro-label is the only uppercase text here, which is what --fs-xs is for. */
.pack-chips-title {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-xs);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 700;
}
.pack-chips {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin: 0 0 var(--sp-4);
  padding: 0;
}
.pack-chip {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-2);
  max-width: 100%;
  padding: 0.3rem 0.65rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card);
  font-size: var(--fs-sm);
}
.pack-chip-name { color: var(--ink); font-weight: 600; }
/* The measure is a measured value, so it is set in the monospace face for the
   same reason the Quick Entry columns are. */
.pack-chip-qty {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--accent-ink);
  white-space: nowrap;
}
/* An item with no quantity says so instead of showing a blank: a chip that
   looked complete when the number is missing is the defect this product hunts. */
.pack-chip-qty.none { color: var(--muted); font-family: inherit; }

/* The things to know before sending, directly above the primary action. Their
   position is the point: read first, download second. */
.pack-checks {
  margin: 0 0 var(--sp-4);
  padding: var(--sp-3);
  border: 1px solid var(--warn-line);
  border-radius: var(--radius-sm);
  background: var(--warn-bg);
}
.pack-checks-title {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-xs);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--warn-ink);
  font-weight: 700;
}
.pack-check-list { margin: 0; padding-left: 1.1rem; }
.pack-check-list li { margin: 0.15rem 0; font-size: var(--fs-sm); }

/* One filled action on this screen. The size sits beside it, quiet, because
   the question it answers ("will this fit in an email") comes later. */
.pack-primary-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  margin: 0;
}
.pack-primary-note { font-size: var(--fs-sm); }

/* File rows: a type badge, the name, the size. A badge instead of a bullet so
   five filenames can be told apart without reading them. */
.pack-rows { padding-left: 0; list-style: none; }
.pack-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2);
  padding: 0.35rem 0;
  border-top: 1px solid var(--line-soft);
}
.pack-row:first-child { border-top: 0; }
.pack-kind {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--accent-ink);
  background: var(--accent-soft);
  border-radius: var(--radius-sm);
  padding: 0.1rem 0.4rem;
  min-width: 2.6rem;
  text-align: center;
}
.pack-row.is-primary a { font-weight: 600; }
.pack-size { font-size: var(--fs-xs); }

/* The folds. The summary carries the heading itself, so a closed fold still
   states what is behind it - the "keep these, do not send them" sentence must
   read even when the block is shut. */
.pack-fold > summary {
  cursor: pointer;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2);
  list-style: none;
}
.pack-fold > summary::-webkit-details-marker { display: none; }
.pack-fold > summary h3,
.pack-fold > summary h4 { margin: 0; }
/* The disclosure triangle is drawn by us so it sits after the heading text at
   every zoom level, and it turns when the fold opens. */
.pack-fold > summary::after {
  content: "";
  width: 0.45rem;
  height: 0.45rem;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(45deg);
  margin-left: auto;
}
.pack-fold[open] > summary::after { transform: rotate(-135deg); margin-top: 0.2em; }
.pack-fold > summary:hover h3,
.pack-fold > summary:hover h4 { color: var(--accent-ink); }
/* A closed heading reads as "not mine" without this line; it says the content
   is one click away rather than gone. */
.pack-fold-hint { color: var(--muted); font-size: var(--fs-xs); }
.pack-fold[open] .pack-fold-hint { display: none; }

/* Folded groups keep the frame their open counterparts have. */
.quickentry.pack-fold {
  margin-top: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line);
  border-top: 1px solid var(--line);
  border-radius: var(--radius);
}
/* The nested plain-text fold keeps the old link-coloured summary; the outer
   one is a heading and must not borrow it. */
.qe-plain-fold > summary {
  cursor: pointer;
  color: var(--accent-ink);
  font-size: var(--fs-base);
}

/* The outcome form folds inside its own section, so the card frame stays and
   only the body collapses. */
.outcome-fold > summary h3 { font-size: var(--fs-lg); }
.outcome-fold > .muted { margin: 0 0 1rem; max-width: 60ch; }

/* The Quick Entry block had one summary before this change (the plain-text
   fold) and its rule tinted every summary inside the block. Now the block
   itself is a fold, so the outer heading has to be taken back off that rule -
   otherwise the estimating-software heading is blue while the identical
   heading on the group above it is not. */
.quickentry.pack-fold > summary { color: var(--ink); }
.pack-fold > summary h3,
.pack-fold > summary h4 { color: var(--ink); }
.pack-fold > summary:hover h3,
.pack-fold > summary:hover h4 { color: var(--accent-ink); }

/* ---------------------------------------------------------------------------
   REVIEW CARD — the approval screen, made scannable (26 Aug 2026)

   Measured complaint, founder watching the demo of his own product: "the app
   looks like a giant dump ... stacked text that nobody reads". The approval
   card was the worst offender because it repeats: thirty items, each printing a
   justification paragraph, an internal block, three risk bullet lists and a
   missing-evidence list, all open at once. The two buttons that are the whole
   point of the screen sat underneath all of it.

   Nothing was deleted. The long prose moved into <details> and the head row
   became tokens. This block styles that head row, the decision row and the
   folds; the package screen's chip idiom is reused rather than reinvented.
   --------------------------------------------------------------------------- */

/* The chip idiom itself already existed twice: .pack-chip on the package screen
   and .chip on the landing page's mock of THIS card. The shared layout was
   lifted into the .chip rule above rather than restated here, so the mock and
   the real thing cannot drift apart. What is left below is the outlined
   variant the card needs - scoped to .candidate, because the landing chips are
   filled and must not grow a border. */
.candidate .chip {
  border: 1px solid var(--line);
  background: var(--card);
  white-space: nowrap;
}

.cand-card { padding: var(--sp-3) var(--sp-4); }

/* The head row is scanned, never read. It wraps rather than truncating,
   because an item description that is cut off is an item nobody can identify. */
.cand-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2);
  margin-bottom: var(--sp-2);
}
/* The item name is the one thing in the row that is allowed to be long, so it
   takes the leftover width and pushes the state label to the far end. */
.cand-name {
  flex: 1 1 14rem;
  min-width: 0;
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--ink);
}
/* A measurement is set in the monospace face for the same reason the Quick
   Entry columns are: it is a number the contractor will compare with another. */
.candidate .cand-qty {
  font-family: var(--mono);
  color: var(--accent-ink);
  border-color: var(--accent-soft);
  background: var(--accent-soft);
}
/* An item with no measurement says so instead of showing a blank chip - a chip
   that looks complete when the number is missing is the defect this product
   hunts, and the sentence inside it is a sentence, not a number. */
.candidate .cand-qty.none {
  font-family: inherit;
  color: var(--muted);
  background: var(--card);
  border-color: var(--line);
  white-space: normal;
}
.cand-head .code { font-family: var(--mono); }
/* Rule 2: a blank code cell is deliberate. It is drawn as an empty cell, never
   as the words "no code", because those words get pasted into an estimate. */
.cand-head .code-blank { color: var(--muted); border-style: dashed; }
.cand-state { margin-left: auto; font-weight: 600; font-size: var(--fs-xs); }

/* Risk is a word, never a number. Low gets a quiet chip rather than no chip:
   an item with no badge reads as an item that was never assessed.

   The three levels are restated against .chip on purpose. The .risk-high and
   .risk-medium rules further up are single-class, so the neutral fill that
   .candidate .chip gives every badge would win on specificity and quietly wash
   the colour out - the one place on this card where colour carries meaning.

   Restated in TOKENS, never in the hard-coded hex those older rules use: a
   literal colour survives the light palette and vanishes on the dark one,
   which is exactly what happened to the link colour (tests/test_link_contrast).
   High therefore reads as an outlined chip rather than a red-filled one; the
   palette has no error background to fill it with, and inventing one would
   open a fourth meaning-carrying colour. */
.chip.risk { font-weight: 700; letter-spacing: 0.03em; }
.chip.risk-medium { background: var(--warn-bg); color: var(--warn-ink); border-color: var(--warn-line); }
.chip.risk-high { background: var(--card); color: var(--error); border-color: var(--error); }
.chip.risk-low { background: var(--card); color: var(--muted); }

/* The two flags keep their own tints (set by an earlier, higher-specificity
   rule) and borrow only the pill shape, so a card cannot end up with round
   chips beside square ones. */
.cand-head .flag { border-radius: 999px; padding: 0.1rem 0.5rem; }

/* The decision row. It sits directly under the head row and outside every
   fold, because a button behind a click is a step that did not happen. */
.cand-decide {
  align-items: flex-end;
  gap: var(--sp-3);
  padding: var(--sp-2) 0 var(--sp-3);
  border-bottom: 1px solid var(--line-soft);
}
/* The quantity box travels with the Include request, so it is laid out as part
   of that button rather than as a form of its own. */
.cand-include {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--sp-2);
}
.cand-include .qty-field { margin-bottom: 0; }
.cand-include .qty-source { flex: 1 1 100%; }

/* The folds. Same mechanism as the package screen: <details>/<summary>, no
   script, closed by default, and the summary states what is behind it. */
.cand-fold {
  margin: var(--sp-2) 0 0;
  border-top: 1px solid var(--line-soft);
  padding-top: var(--sp-2);
}
.cand-fold > summary {
  cursor: pointer;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  list-style: none;
}
.cand-fold > summary::-webkit-details-marker { display: none; }
.cand-fold > summary h3 { margin: 0; }
/* The disclosure triangle is drawn here so it sits at the end of the row at
   every zoom level, and it turns when the fold opens. */
.cand-fold > summary::after {
  content: "";
  width: 0.4rem;
  height: 0.4rem;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(45deg);
  margin-left: auto;
}
.cand-fold[open] > summary::after { transform: rotate(-135deg); margin-top: 0.2em; }
.cand-fold-title { font-weight: 600; font-size: var(--fs-sm); }
.cand-fold > summary:hover .cand-fold-title { color: var(--accent-ink); }
/* How many items are behind the fold, as a bare numeral. A word next to it
   would need a plural, and a plural built in the template splits the sentence
   the customer reads away from messages.py. */
.cand-fold-count {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--warn-ink);
  background: var(--warn-bg);
  border-radius: var(--radius-sm);
  padding: 0 0.4rem;
}
/* The developer identifier for the argument. It is kept (support conversations
   name items by it) but it is not head-row material: it is the kind of string
   that makes a contractor ask what he is looking at. */
.cand-meta { margin: 0.2rem 0; font-size: var(--fs-xs); color: var(--muted); }
.cand-meta .argument { font-family: var(--mono); }

/* The internal block folds like the rest, but its warning IS the summary, so
   the audience boundary reads with the block shut. The frame stays amber even
   when closed - the colour is part of the warning, not decoration on the body. */
.internal-block.cand-fold { border-top: 0; }
.internal-block.cand-fold > summary .internal-title { font-size: var(--fs-xs); }

/* The risk fold keeps its own frame; it was already a <details> before this
   change and only lost its default-open state. */
.risk-block.cand-fold { border-top: 1px solid var(--line); }


/* ---------------------------------------------------------------------------
   STEP DENSITY (26 Aug 2026)

   Measured on a 1920x1080 viewport with a rich job (estimate, six photographs,
   facts filled, candidates, a built package): every step of the wizard was
   1650-2340px tall, so every step scrolled. The founder, watching his own
   product: "there is STILL too much detail; I do not want downward scrolling -
   it should feel like NEXT, NEXT".

   Nothing was deleted. Four blocks became folds, and this section styles them.
   Rule 9 holds: a closed <details> keeps its content in the DOM and one click
   opens it, with no script involved.
   --------------------------------------------------------------------------- */

/* --- 1. The photo report, when there is nothing to report ------------------
   The block is drawn on every step and cost 181px even on a job where nothing
   went wrong. When every photograph was read, the sentence that says so IS the
   summary; the counts table and its heading sit one click behind it. When a
   photograph was NOT read the block keeps its open form - a folded warning is
   a warning nobody finds. */
.budget-quiet {
  margin-top: var(--sp-4);
  padding-top: var(--sp-3);
}
.budget-fold > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--muted);
  font-size: var(--fs-sm);
}
.budget-fold > summary::-webkit-details-marker { display: none; }
.budget-fold > summary::after {
  content: "";
  width: 0.4rem;
  height: 0.4rem;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(45deg);
}
.budget-fold[open] > summary::after { transform: rotate(-135deg); margin-top: 0.2em; }
.budget-fold > summary:hover { color: var(--accent-ink); }
/* The heading is inside the fold now, so it must not keep the top margin the
   open block gave it. */
.budget-fold > h3 { margin-top: var(--sp-3); }

/* --- 2. The facts accordion ------------------------------------------------
   Sections 1 and 2 became <details> so that the whole form is one accordion.
   They carry .step-lead rather than .step-fold because they are the required
   part of the path, not the optional part; the two classes look identical
   today and the declarations below are a deliberate copy of the .step-fold
   block above. Merging them would mean editing a rule that is already shipped,
   and this stylesheet is only ever appended to. */
.step-lead {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  padding: 0;
  margin-bottom: 1.5rem;
}
.step-lead > summary {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  flex-wrap: wrap;
  margin: 0;
  padding: 0.6rem 2.25rem 0.6rem 0.85rem;
  border-radius: var(--radius);
  background: var(--accent-soft);
  cursor: pointer;
  list-style: none;
  position: relative;
}
.step-lead > summary h3 {
  margin: 0;
  font-size: var(--fs-base);
  font-weight: 650;
  color: var(--accent-strong);
}
.step-lead > summary::-webkit-details-marker { display: none; }
.step-lead > summary::after {
  content: "";
  position: absolute;
  right: 1rem;
  top: 50%;
  width: 0.5em;
  height: 0.5em;
  margin-top: -0.45em;
  border-right: 2px solid var(--accent-ink);
  border-bottom: 2px solid var(--accent-ink);
  transform: rotate(45deg);
  transition: transform 200ms ease-out;
}
.step-lead[open] > summary { border-radius: var(--radius) var(--radius) 0 0; }
.step-lead[open] > summary::after { transform: rotate(-135deg); margin-top: -0.15em; }
/* The threshold question sits inside section 2's body; it kept its own frame
   and only needs its first-child margin removed inside the fold. */
.step-lead .fold-body > .facts-grid + .threshold { margin-top: 1.25rem; }

/* --- 3. The delete card ----------------------------------------------------
   The most destructive thing this product can do sits at the foot of every
   step, and it cost 183px there. The title is now the summary; the sentence
   that explains it, the two-step confirm and the button itself are one click
   behind it. The confirm is still two steps: this outer fold opens the section,
   it does not delete anything. */
.delete-card-fold > summary {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  cursor: pointer;
  list-style: none;
}
.delete-card-fold > summary::-webkit-details-marker { display: none; }
.delete-card-fold > summary h2 { margin: 0; }
.delete-card-fold > summary::after {
  content: "";
  width: 0.45rem;
  height: 0.45rem;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(45deg);
}
.delete-card-fold[open] > summary::after { transform: rotate(-135deg); margin-top: 0.2em; }
.delete-card-fold > summary:hover h2 { color: var(--error); }

/* --- 4. The pre-send checks ------------------------------------------------
   Still above the primary action, still in the amber frame, but folded: five
   warning lines were 206px in the middle of the reward moment. A closed
   <details> keeps its children in the DOM, so the measured ordering rule
   ("everything to know before sending comes before every download link")
   is unchanged. */
.pack-checks > summary { margin: 0; }
.pack-checks > summary .pack-checks-title { margin: 0; }
.pack-checks[open] > summary { margin-bottom: var(--sp-2); }

/* ===========================================================================
   STEP DENSITY, SECOND PASS (26 Aug 2026)

   The first pass folded four blocks and the four steps came down to
   1083 / 1217 / 920 / 1389px of frame. Two of them still missed the 1150px
   target and the whole document still scrolled past 1080 on all four, so the
   remainder was measured rather than argued about (headless Chromium at
   1920x1080, a rich job):

     header 62 + page padding 72 + trailing card gap 32 + footer gap 48
     + footer 262 = 476px of chrome on EVERY step
     three stacked cards per step = 144px of padding + 64px of gaps

   None of that carries a sentence. This pass takes back only that space: the
   vertical rhythm of a marketing page, applied to a wizard that draws three
   cards on every screen. Nothing is hidden and nothing is folded here; Rule 9
   is not even in play, because no content is touched.
   --------------------------------------------------------------------------- */

/* --- 5. The job page runs at its own rhythm --------------------------------
   .card is sized for a page that carries one or two of them. Every step of the
   job page carries three - the run band, the step itself, the delete row - so
   the same padding and the same gap are paid three times per screen. */
#job-frame .card {
  padding: var(--sp-4) 1.25rem;
  margin-bottom: var(--sp-4);
}
/* The delete row is the last thing in the frame. The gap under it only pushed
   the footer further down the page. */
#step-body > .card:last-child { margin-bottom: 0; }

/* --- 6. The run band is the head of the page, not a card -------------------
   It carries the step bar, one sentence about where the job is, and two folds.
   Drawing a card around it put a second frame inside the page frame and paid
   for it in padding on every step. A rule under it separates it from the step
   just as well as a border around it did. */
#job-frame > #status-card {
  padding: 0 0 var(--sp-3);
  background: none;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  box-shadow: none;
}
/* The bar, the sentence and the two folds are one block, so they are set at
   one rhythm instead of four inherited ones. */
.jobsteps { margin-bottom: var(--sp-4); }
#job-status > h2 { margin-bottom: var(--sp-1); }
#job-status > p { margin-bottom: var(--sp-2); }
#job-status > .job-quiet { margin: 0; }
#job-status > .budget-quiet { margin-top: var(--sp-2); padding-top: var(--sp-2); }

/* --- 7. The facts accordion is one list, not five cards --------------------
   Five sections with 1.5rem between them spent 120px on gaps while four of the
   five were closed. An accordion reads as one control; its rows sit together. */
.facts-section { margin-bottom: var(--sp-2); }
.facts-actions { margin-top: var(--sp-3); }

/* --- 8. The package screen -------------------------------------------------
   The reward moment was the tallest step (1389px). Its own blocks were already
   folded in the first pass; what was left was spacing: a band with 1rem of
   padding and 1rem under every child, file rows at 40px each, and a 0.75rem
   gap before each of the four groups under it. */
.pack-result { padding: var(--sp-3); margin-bottom: var(--sp-3); }
.pack-meta { margin-bottom: var(--sp-2); }
.pack-chips { margin-bottom: var(--sp-3); }
.pack-checks { margin-bottom: var(--sp-3); padding: var(--sp-2) var(--sp-3); }
.filegroup { margin-top: var(--sp-2); }
.pack-row { padding: var(--sp-1) 0; }

/* --- 9. The signed-in footer is a strip ------------------------------------
   Same brand, same tagline, same Resources and Legal links, same closing
   sentence - laid out in one row instead of four columns. The three legal
   links stay in the navigation, which is what CalOPPA and the payment
   provider's review look for. The marketing column already used this same
   signed-in/signed-out split, so nothing new is being decided here.
   The wide-screen grid is what costs the height, so the flex row is applied
   from the tablet breakpoint up; on a phone the stacked columns stay. */
@media (min-width: 721px) {
  .footer-app { margin-top: var(--sp-5); }
  .footer-app .footer-inner { padding: var(--sp-4) 1.25rem var(--sp-3); }
  .footer-app .footer-grid {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-3) var(--sp-6);
  }
  .footer-app .footer-brand { max-width: none; }
  .footer-app .footer-brand p { margin: 0; max-width: none; }
  .footer-app .footer-col {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-1) var(--sp-4);
  }
  .footer-app .footer-head { margin: 0; }
  .footer-app .footer-note { margin-top: var(--sp-4); padding-top: var(--sp-3); }
}

/* --- 10. The tail of the job page ------------------------------------------
   48px of page padding, then 48px of footer margin, under a page that was
   already too tall to fit. */
main:has(#job-frame) { padding-bottom: var(--sp-5); }
main:has(#job-frame) ~ .footer { margin-top: 0; }

/* --- 8b. The package screen, measured again ---------------------------------
   After 8 the step still stood at 1211px of frame against a 1150 target, so the
   band was measured child by child instead of trimmed by feel. What was left
   was all separation: two muted lines above the file list carrying a full
   paragraph gap each, a 0.4rem gap before the list itself, and 44px of margin
   plus padding above the outcome fold at the foot of the screen. */
#package .filegroup > p.muted { margin-bottom: var(--sp-1); }
#package .filelist { margin-top: var(--sp-1); }
#package .filegroup { padding: var(--sp-2) var(--sp-3); }
#package .pack-chip { padding: 0.2rem 0.6rem; }
/* The outcome fold is the last block on the last step. It keeps its rule above
   it - that line is what says "this part comes after you send it" - but not
   three quarters of an inch of air on both sides of it. */
#package .outcome { margin-top: var(--sp-3); padding-top: var(--sp-3); }
/* The Quick Entry fold is one of four groups under the band and reads as one of
   them, so it is framed like them: it was carrying a 1rem gap and 0.75/1rem of
   padding of its own, from the days when it was the only block down here. */
#package .quickentry.pack-fold { margin-top: var(--sp-2); padding: var(--sp-2) var(--sp-3); }
/* The two quiet lines above the carrier list - what the list is, and what it
   weighs - are one line, not two. Both are about the same list and both are
   read in the same glance; stacked, they cost 60px above four file rows.
   Nothing is hidden: the paragraphs are untouched in the markup, they simply
   flow into one line when there is room, and wrap when there is not. */
#package .filegroup > p.muted { display: inline; }

/* ---------------------------------------------------------------------------
   APPROVAL SCREEN, FOCUSED MODE (26 Aug 2026)

   Measured: "the include side looks terrible and confusing - replan it from
   scratch. The system should feel like NEXT, NEXT." The compact card landed
   the same morning and made ONE card readable; nine of them were still a wall.
   The screen now shows one item, and deciding it brings the next.

   Nothing here restyles the card itself: the focused panel is a frame around
   the card that already exists, so the two views cannot drift apart. What is
   new is the progress head, the decided list and the switch link.

   Tokens only, no literal colours - a hard-coded colour survives the light
   palette and vanishes on the dark one, which is what happened to the link
   colour (tests/test_link_contrast.py).
   --------------------------------------------------------------------------- */

/* The head is one quiet block: where you are, what you have done, how far it
   goes. It is separated by a rule rather than by a heading, because a heading
   here would compete with the item name in the card below it. */
.focus-head {
  margin-bottom: var(--sp-4);
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--line);
}
/* The position is the answer to "how much longer is this", so it is the
   largest thing in the head and the only thing set in the ink colour. */
.focus-position {
  margin: 0;
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--ink);
}
.focus-counts {
  margin: var(--sp-1) 0 var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--muted);
}
/* The bar borrows .run-meter whole - same element, same three tints, same
   no-script rule. Only the space around it belongs to this screen. */
.focus-head .run-meter { margin-bottom: var(--sp-3); }
.focus-intro,
.focus-carried { margin: 0 0 var(--sp-1); font-size: var(--fs-sm); }

/* The completion state. It reads as an arrival, not as an empty list: the
   sentence says the work is behind the contractor and the next action is
   named. The build button itself is not repeated here - it is pointed at. */
.focus-done {
  margin-top: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--ok);
  border-radius: var(--radius);
  background: var(--ok-soft);
}
.focus-done h2 { margin: 0; font-size: var(--fs-lg); color: var(--ok-ink); }
.focus-done p { margin: var(--sp-2) 0 0; }
.focus-done-build { font-weight: 600; }

/* The decided list is the way back. Closed while there is still an item
   waiting - what is worth reading then is the card above it - and open once
   nothing is waiting, because going back is the only thing left to do. */
.focus-decided {
  margin-top: var(--sp-4);
  border-top: 1px solid var(--line);
  padding-top: var(--sp-3);
}
.focus-decided > summary {
  cursor: pointer;
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  font-weight: 600;
}
.focus-decided-count {
  font-size: var(--fs-xs);
  padding: 0.05rem 0.45rem;
  border-radius: 999px;
  background: var(--bg);
  color: var(--muted);
}
.focus-decided-list { list-style: none; margin: var(--sp-2) 0 0; padding: 0; }
/* One row per item: identifier, name, state, undo. The row wraps rather than
   truncating, for the same reason the card head does - an item description
   that is cut off is an item nobody can identify. */
.focus-decided-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2);
  padding: var(--sp-2) 0;
  border-top: 1px solid var(--line-soft);
}
.focus-decided-link { font-family: var(--mono); font-size: var(--fs-sm); }
.focus-decided-name { flex: 1 1 12rem; min-width: 0; }
.focus-decided-row .state-label { font-size: var(--fs-xs); font-weight: 600; }
.focus-decided-row.state-included .state-label { color: var(--ok-ink); }
.focus-decided-row.state-excluded .state-label { color: var(--muted); }
.focus-undo { margin: 0; }

/* The switch between the two views. It is quiet but it is never absent: with
   this link gone the focused view stops being a lens and becomes a filter that
   hides items without saying so. */
.focus-switch { margin: 0 0 var(--sp-3); font-size: var(--fs-sm); }
/* In the focused panel the link is the last thing on the card, so it takes a
   rule above it; in the full list it is the first thing and takes none. */
.review-focus .focus-switch {
  margin: var(--sp-4) 0 0;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line-soft);
}

/* ===========================================================================
   LINEAR FLOW (27 Aug 2026). The founder walked the finished product again and
   repeated the same sentence: "NEXT NEXT - a linear flow, no separate places,
   no noise." Five things changed and only these rules came with them; every
   colour is a token, every size comes from the type scale.
   =========================================================================== */

/* --- 1. the upload box: its own title row, its own files ------------------ */

/* The title and its (i) sit on one line. The title rule above declares
   `display: block`, which would push the mark onto a line of its own; the
   wrapper takes the block and leaves the two children side by side. */
.upload-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

/* The file a box is holding, directly under that box. It was a card-level
   list above all three boxes: a contractor looking at the measurement box read
   that box's file name at the top of the screen, and told the two apart only
   by the field name printed beside them. Same rows, grouped by the box that
   owns them. */
.uploadbox .uploadlist {
  margin: var(--sp-2) 0 0;
}
.uploadbox .uploadlist li {
  gap: var(--sp-2);
  padding: var(--sp-2) 0 0;
}
/* The name is allowed to take the room and to wrap: a truncated file name is a
   file nobody can identify (the same reason the candidate head wraps). */
.uploadbox .uploadlist .filename {
  flex: 1 1 10rem;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* --- 2. the step's forward link is filled --------------------------------- */

/* `.stepnav-next` used to be a quiet link, on the argument that the card above
   owns the primary action. Measured against the real screen the argument lost:
   the forward link sits OUTSIDE the card, alone at the foot of the page, and a
   thin link there did not read as an action at all. It is still an <a> with a
   real href - only the paint changed (a.primary above). */
.stepnav-next.primary {
  /* The filled shape must not stretch across the row the way the mobile
     `a.primary` rule makes it; it stays a pill on the right. */
  align-self: flex-start;
}

/* --- 3. the second submit on the job facts form -------------------------- */

/* "Save and read" is the shortcut, not the main button: the plain `button`
   rule already draws the outlined shape, and this rule only keeps the two
   words on one line beside the filled "Save and continue". */
.facts-read { white-space: nowrap; }

/* --- 4. the approval panel, embedded in step 3 --------------------------- */

/* On its own page the panel is the first card on the screen. Inside the step
   body it follows the Review card, so it takes the gap the card stack uses.
   Nothing else about the panel changed - it is the same partial. */
#step-body .review-focus { margin-top: var(--sp-5); }
/* The counter comes with it, because the completion state points at
   #approval-counter and an anchor with no target does nothing at all. */
#step-body .counter {
  margin: var(--sp-5) 0 0;
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
}

/* The refusal slot for "Save and read". `.facts-actions` is a flex row; the
   notice and its fix link take a full row of their own so the message is not
   squeezed beside two buttons. The link stays hidden until the box fills - the
   rule for that is the shared `.form-notice:not(:empty) + .notice-fix` above. */
.facts-actions .form-notice,
.facts-actions .notice-fix {
  flex: 1 0 100%;
  margin-top: 0;
}

/* --- 5. the second bar on step 3 ----------------------------------------- */

/* The approval panel's bar keeps borrowing `.run-meter` whole - same element,
   same three tints, same no-script rule - and now also carries `.review-meter`
   as its own NAME. No paint hangs off that name: it exists because the panel
   moved into step 3, so the run bar and the approval bar can appear on one
   page, and a gate that says "the run bar is gone once the run is over" was
   reading the approval bar instead. Two different things must not answer to
   one name; the same collision cost the upload bar its height once
   (`.progress`, 23 Aug 2026). */

/* ==========================================================================
   NOTICE FIX SCOPE (27 Aug 2026). The review repair link used to open the
   moment the notice slot was non-empty, so every unrelated form mistake also
   showed "Open Review, where this file is paid for". CSS cannot read the
   refusal text; the server names the repair in an X-Notice-Fix response
   header and intake.js sets .notice-fix-on only then. The first rule turns
   the old any-notice reveal off (later in the file, same specificity, so it
   wins); the second opens the link only when the script confirmed it.
   ========================================================================== */
.form-notice:not(:empty) + .notice-fix { display: none; }
.form-notice:not(:empty) + .notice-fix.notice-fix-on { display: block; }
