/* Siding of Atlanta brand tokens.
   Real, confirmed values -- do not approximate, invent alternates, or fall
   back to a generic palette. See docs/stage-2-5-frontend-interaction-spec.md. */

:root {
  --evergreen: #26593B;   /* primary */
  --ochre:     #CA7E31;   /* accent */
  --bg:        #FAF7F1;   /* background */
  --text:      #261D16;   /* text */
  --surface:   #F4F0E7;   /* supporting */
  --muted:     #665B52;   /* supporting */
  --rule:      #DDD6CC;   /* supporting */

  --font-heading: "Fraunces", Georgia, serif;
  --font-body: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;

  --maxw: 1120px;

  /* Radius (2.4). Controls keep 6px; cards, callouts and the document get
     10px, because a card at 6px reads as a form control. */
  --radius: 6px;
  --radius-card: 10px;

  /* Type scale (2.1). Nine steps on a 1.25 ratio plus one display step.
     These are the ONLY font sizes in this file -- the one deliberate
     exception is .scope li.scope-total strong, which section 2.5 pins at
     1.3rem so the customer document does not shout.

     --t-bid (4rem) is gone (rehearsal, 2026-09-22). It existed so the bid
     could be the largest thing in the app; the bid is now one of four figures
     at --t-hero, and a step nothing uses is a step that invites reuse for the
     wrong reason. */
  --t-caption: 0.75rem;
  --t-small:   0.875rem;
  --t-body:    1rem;
  --t-lead:    1.125rem;
  --t-h3:      1.25rem;
  --t-h2:      1.5rem;
  --t-h1:      2.25rem;
  --t-hero:    3rem;

  /* Spacing scale (2.2). Replaces --gap and every ad hoc rem value. */
  --s-1: 0.25rem;
  --s-2: 0.5rem;
  --s-3: 0.75rem;
  --s-4: 1rem;
  --s-5: 1.5rem;
  --s-6: 2rem;
  --s-7: 3rem;
  --s-8: 4rem;

  /* Surfaces (2.3). Three, kept distinct: --bg is the page, --card is the
     raised sheet, --surface is inset. */
  --card: #FFFFFF;

  /* The literal hex values that used to sit inline in component rules. */
  --evergreen-deep: #1d4830;   /* primary button hover */
  --ochre-wash:     #fdf6ec;   /* callout background */
  --alert-wash:     #fbeeea;   /* suspect callout, flagged field */

  /* One elevation, one lift, one duration. The shadows are tinted with the
     text colour rather than grey, so a card reads as paper on a desk. */
  --shadow-card: 0 1px 2px rgba(38, 29, 22, 0.06), 0 4px 16px rgba(38, 29, 22, 0.05);
  --shadow-lift: 0 2px 4px rgba(38, 29, 22, 0.08), 0 12px 32px rgba(38, 29, 22, 0.08);
  --motion: 150ms ease-out;

  /* Fraunces carries an optical-size axis, already loaded by the Google
     Fonts link. Letting it track the size gives the display cut at --t-hero
     and the text cut at --t-h3. */
  font-optical-sizing: auto;
}

/* One alert colour, for a blocked line item and the suspect-group callout.
   Deliberately not a new brand hue -- a desaturated red so nothing in the
   palette competes with evergreen for "this is fine".

   There is no longer a parallel tier colour scale: the window-review tiers
   are carried entirely by the badge treatments (filled / outlined / bare
   text), and a second colour system running alongside them added noise
   rather than reinforcement. */
:root {
  --alert: #A33A22;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.5;
}

h1, h2, h3, h4 {
  font-family: var(--font-heading);
  font-weight: 600;
  line-height: 1.2;
  margin: 0 0 0.5em;
}

h1 { font-size: var(--t-h1); line-height: 1.05; margin-bottom: var(--s-2); }
h2 { font-size: var(--t-h2); }
h3 { font-size: var(--t-h3); }

/* The one-sentence intro under a page title. The spec gives the title block
   more air than the old 0.5em: --s-2 under the h1, --s-6 before the first card. */
h1 + p { font-size: var(--t-lead); color: var(--muted); margin: 0 0 var(--s-6); }

a { color: var(--evergreen); }

/* ---------- layout ---------- */

.site-header {
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
  padding: var(--s-3) var(--s-5);

  /* The nav is how you move around a screen you are scrolling on a tablet, so
     it stays put. Above the cards, which carry --shadow-card and would
     otherwise draw over it. */
  position: sticky;
  top: 0;
  z-index: 10;
  transition: padding var(--motion);
}

/* Once the page has scrolled past 80px the header gives some height back.
   base.html toggles the class; only the logo and the padding move, so the
   nav does not reflow under a thumb that is mid-reach. */
.site-header.compact { padding-top: var(--s-2); padding-bottom: var(--s-2); }
.site-header.compact img.logo { height: 36px; }

/* Two rows, stacked: identity and status, then the steps. The header stopped
   trying to be one bar because one bar wrapped differently in each iPad
   orientation. */
.site-header .inner {
  max-width: var(--maxw);
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--s-2);
}

/* Row 1: which tool, which quote, and the way around. Never wraps at any width. */
.header-id {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--s-4);
  min-width: 0;
}

/* 56px, not 46: the logo now carries the "We're on Your House's Side"
   tagline under the wordmark, so the same wordmark size needs the extra
   height (436px tall now vs 351px before, at the same 1467px width). */
.site-header img.logo {
  height: 56px;
  width: auto;
  display: block;
  transition: height var(--motion);
}

/* Which tool: the logo, a quiet divider, and the tool's name. The divider is
   the border grey, not ochre, which means "this needs you" everywhere else.
   The name is in the body text colour: evergreen is what can be tapped, and
   this cannot. None of it shrinks: the label is shown whole, or below 900px
   dropped, never shortened. A shrink weight is not enough -- at 1 against the
   quote's 1000 the label still lost a fraction of a pixel, and Safari on the
   iPad drew that as "Siding Estimat...". */
.header-tool { display: flex; align-items: center; gap: var(--s-3); flex: none; }
.site-header .header-tool img.logo { flex: none; }
.tool-divider { flex: none; width: 1px; align-self: stretch; margin: var(--s-2) 0; background: var(--rule); }
.tool-name {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: var(--t-h3);
  color: var(--text);
  white-space: nowrap;
  flex: none;
}

/* Which quote. A grid, so the quote number sets the slot's minimum width:
   the customer's column may shrink to nothing, the number's never does, and
   Randy reads the number aloud. The customer name is the only thing in row
   1 that gives way. */
.header-quote { display: flex; flex: 1 1 auto; }
.quote-ref {
  display: grid;
  grid-template-columns: max-content max-content minmax(0, max-content);
  align-items: center;
  column-gap: 0.4em;
  min-height: 44px;
  font-weight: 600;
  text-decoration: none;
}
.quote-ref.here { color: var(--text); }
/* What goes somewhere says so (iPad check, 2026-10-04): a house before the
   reference where it links to the job's summary page, an arrow before "Back
   to". Sized to the text, in the link's own colour. The house sits in the
   reference's left padding rather than in a fourth grid column, so the
   number keeps its column and the customer name is still the only thing in
   row 1 that gives way. */
.link-icon { width: 1em; height: 1em; flex: none; display: block; }
a.quote-ref { position: relative; padding-left: 1.4em; }
a.quote-ref > .home-icon { position: absolute; left: 0; top: 50%; transform: translateY(-50%); }
.quote-number { white-space: nowrap; font-variant-numeric: tabular-nums; }
.quote-sep { color: var(--muted); }
.quote-customer { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
/* On the list the slot holds only "Back to ...": no number to protect, so
   it may shrink to nothing, and the link truncates. */
.header-quote.on-list { min-width: 0; }
.header-quote .back-to { min-width: 0; }

/* The way around: the saved time, then All quotes, at the right end. Never
   shrinks. */
.header-nav { display: flex; align-items: center; gap: var(--s-3); flex: none; margin-left: auto; }

main {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: var(--s-6) var(--s-5) var(--s-8);
}

/* Row 2: the eight steps, always 1 to 8 in one row. When they do not fit the
   strip scrolls sideways inside itself, with the platform's own momentum;
   the page never does. At desktop width all eight fit and nothing moves.
   stepnav.js opens it on the current step and sets the edge fades. */
.stepnav {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scroll-behavior: smooth;
  gap: var(--s-2);
  min-width: 0;
  /* A visible scrollbar under a nav is noise; the fade says it continues. */
  scrollbar-width: none;
  font-size: var(--t-caption);
  font-weight: 500;
}
.stepnav::-webkit-scrollbar { display: none; }

/* A strip that looks complete but is not is worse than a wrap, so the edge
   with more beyond it fades out. */
.stepnav.more-right {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 3rem), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 3rem), transparent);
}
.stepnav.more-left {
  -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 3rem), transparent);
  mask-image: linear-gradient(to left, #000 calc(100% - 3rem), transparent);
}
.stepnav.more-left.more-right {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 3rem, #000 calc(100% - 3rem), transparent);
  mask-image: linear-gradient(to right, transparent, #000 3rem, #000 calc(100% - 3rem), transparent);
}

/* Each step is one target: name on the first line, the count in words on
   the second. 44px each way from padding rather than bigger type, and a
   hairline border so neighbours read as separate things to a thumb. */
.stepnav a {
  flex: 0 0 auto;
  display: inline-flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  min-height: 44px;
  min-width: 44px;
  padding: var(--s-1) var(--s-3);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  text-decoration: none;
  color: var(--muted);
  white-space: nowrap;
  transition: background-color var(--motion), color var(--motion), border-color var(--motion);
}

.stepnav .step-name {
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.stepnav a:hover:not([aria-current]) { background: var(--rule); color: var(--text); }

/* ---------- surfaces ---------- */

.card {
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--radius-card);
  padding: var(--s-5);
  margin-bottom: var(--s-5);
  box-shadow: var(--shadow-card);
  transition: box-shadow var(--motion), border-color var(--motion);
}

.muted { color: var(--muted); }
.small { font-size: var(--t-small); }

/* ---------- forms ---------- */

label { display: block; font-weight: 500; margin-bottom: 0.25rem; }

input[type="text"], input[type="number"], input[type="tel"], select, textarea {
  font-family: var(--font-body);
  font-size: var(--t-body);
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--text);
  width: 100%;
  transition: border-color var(--motion), box-shadow var(--motion);
}

input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--evergreen);
  outline-offset: 1px;
}

button, .btn {
  font-family: var(--font-body);
  font-size: var(--t-body);
  font-weight: 500;
  padding: var(--s-2) var(--s-4);
  border: 1px solid var(--evergreen);
  border-radius: var(--radius);
  background: var(--evergreen);
  color: #fff;
  cursor: pointer;
  text-decoration: none;
  /* inline-flex, not inline-block, so min-height is a real floor and the label
     stays centred in it. 38px before this. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  transition: background-color var(--motion), border-color var(--motion),
              box-shadow var(--motion), transform var(--motion);
}

/* A primary button lifts a pixel on hover and drops back when pressed. */
button:hover, .btn:hover { background: var(--evergreen-deep); transform: translateY(-1px); }
button:active, .btn:active { transform: translateY(0); }

button.secondary, .btn.secondary {
  background: var(--card);
  color: var(--evergreen);
}

button.secondary:hover, .btn.secondary:hover { background: var(--surface); }

/* ---------- tables ---------- */

table { border-collapse: collapse; width: 100%; font-size: var(--t-body); }
th, td { text-align: left; padding: var(--s-2) var(--s-3); border-bottom: 1px solid var(--rule); }
th { font-weight: 600; color: var(--muted); font-size: var(--t-caption); text-transform: uppercase; letter-spacing: 0.06em; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }

/* ---------- status + tier badges ---------- */

.badge {
  display: inline-block;
  font-size: var(--t-caption);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: var(--s-1) var(--s-2);
  border-radius: 3px;
  border: 1px solid currentColor;
}

/* Also used for "Confirmed" alongside a tier badge on an already-done
   window row -- a quiet outline that never competes with it. */
.badge.computed { color: var(--evergreen); }
.badge.needs_confirmation { color: var(--ochre); }
.badge.blocked_missing_rate { color: var(--alert); }

.callout {
  border: 1px solid var(--ochre);
  background: var(--ochre-wash);
  border-radius: var(--radius-card);
  padding: var(--s-3) var(--s-4);
  margin: var(--s-4) 0;
  font-size: var(--t-small);
}

.callout.suspect { border-color: var(--alert); background: var(--alert-wash); }

/* ---------- window review ---------- */

.checklist .progress {
  height: 8px;
  background: var(--rule);
  border-radius: 99px;
  overflow: hidden;
  margin: 0.5rem 0;
}
.checklist .progress span { display: block; height: 100%; background: var(--evergreen); }

.group-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
.group-head h3 { margin-bottom: var(--s-1); }
/* Member codes and the opening count are metadata, not the heading. */
.group-head .small { font-size: var(--t-small); }

/* Tier badges vary WEIGHT and SHAPE, not only hue. Outline-against-outline
   made medium and high read as near-identical at a glance, and using one
   accent for both suspect and no-value grouped two rows that mean different
   things. The primary separation is now filled vs. outlined vs. bare text,
   which reads faster than a colour difference and survives being skimmed.

   Suspect and no-value are BOTH filled, because both mean real work -- but
   in different colour families, because they mean different kinds of work:
   "a number came back and should not be trusted" vs. "no number came back". */

/* suspect -- solid ochre, dark text, bold. The loudest treatment on the
   screen: a confident number is sitting there and must not be confirmed. */
.badge.suspect {
  background: var(--ochre);
  border-color: var(--ochre);
  color: var(--text);
  font-weight: 700;
}

/* no-value -- solid muted brown, cream text. Equally real work, different
   kind: nothing was produced and Randy reads the width off the diagram. */
.badge.none {
  background: var(--muted);
  border-color: var(--muted);
  color: var(--bg);
  font-weight: 600;
}

/* medium -- outline only, normal weight. A real decision, but one that
   starts from a number rather than from nothing. */
.badge.medium {
  background: none;
  border-color: var(--muted);
  color: var(--muted);
  font-weight: 400;
}

/* high -- no border, no fill, small evergreen text. The lightest possible
   mark: one tap to confirm, and nothing to deliberate over. */
.badge.high {
  background: none;
  border: none;
  color: var(--evergreen);
  font-weight: 500;
  padding-left: 0;
  padding-right: var(--s-1);
}

.confirm-form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4);
  align-items: flex-start;
  margin-top: var(--s-4);
  padding-top: var(--s-4);
  border-top: 1px solid var(--rule);
}
.confirm-form .field { display: flex; flex-direction: column; min-width: 8.5rem; }
/* Width and height are what this screen is for: the inputs are the largest
   controls in the app and their captions the smallest text on the card. */
.confirm-form .ft-in input {
  font-size: var(--t-h3);
  font-variant-numeric: tabular-nums;
}
/* Feet and inches side by side, each box followed by its unit, as Hover
   writes 7'5". Feet are at most two digits; inches may carry a decimal. */
.confirm-form .ft-in { display: flex; align-items: baseline; gap: var(--s-1); }
.confirm-form .ft-in input[name$="_feet"] { width: 4.5rem; }
.confirm-form .ft-in input[name$="_inches"] { width: 5.5rem; }
.confirm-form .ft-in .unit { color: var(--muted); margin-right: var(--s-2); }
.confirm-form .field > .small {
  font-size: var(--t-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.confirm-form .field.actions { justify-content: flex-end; align-self: flex-end; }
.confirm-form input { width: 8.5rem; }
.confirm-form .small { margin-top: 0.2rem; }

.perimeter {
  font-family: var(--font-heading);
  font-size: var(--t-h3);
  padding: var(--s-2) 0;
  font-variant-numeric: tabular-nums;
}

.diagram { margin: 0.75rem 0; }
.diagram summary { cursor: pointer; font-size: var(--t-small); color: var(--muted); }
.diagram img {
  max-width: 100%;
  border: 1px solid var(--rule);
  border-radius: var(--radius-card);
  margin-top: var(--s-2);
  background: var(--card);
}

.stream { background: var(--surface); border-radius: var(--radius-card); padding: var(--s-3) var(--s-4); margin: var(--s-4) 0; }
.stream h4 { margin: 0 0 0.4rem; color: var(--muted); }
ol.attempts { margin: 0; padding-left: var(--s-5); font-size: var(--t-small); }
/* Each page attempt arrives over SSE. Fading it in makes the search read as
   landing rather than popping. */
@keyframes attempt-in { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: none; } }
ol.attempts li { margin-bottom: var(--s-2); animation: attempt-in var(--motion) both; }
ol.attempts li.miss { color: var(--muted); }
ol.attempts li.hit { color: var(--evergreen); }
ol.attempts li.final { color: var(--text); font-weight: 600; border-top: 1px solid var(--rule); padding-top: var(--s-2); }
ol.attempts em { color: var(--muted); font-style: italic; }

form.inline { display: inline; }
button.linkish {
  background: none; border: none; color: var(--evergreen);
  text-decoration: underline; padding: 0; font-size: var(--t-small); cursor: pointer;

  /* 17px of text, 44px of target. The hit box below is invisible and centred
     on the glyphs, so "undo" and "remove" look exactly as they did and are
     merely reachable with a finger. */
  position: relative;
  display: inline-block;
  min-height: 1.4em;
}
button.linkish::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  height: 44px;
  min-width: 44px;
}
button.linkish:hover { background: none; }

/* ---------- big 4 ---------- */

.methods { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: var(--s-4); margin-bottom: var(--s-5); }
.method {
  background: var(--card); border: 1px solid var(--rule); border-radius: var(--radius-card);
  padding: var(--s-4); cursor: pointer; display: flex; flex-direction: column; gap: var(--s-1);
  font-weight: 400;
  box-shadow: var(--shadow-card);
  transition: border-color var(--motion), box-shadow var(--motion), transform var(--motion);
}
.method:hover { border-color: var(--evergreen); transform: translateY(-1px); }
/* The method Randy chose sits visibly above the three he didn't. */
.method.chosen {
  border-color: var(--evergreen); border-width: 2px; background: var(--surface);
  box-shadow: var(--shadow-lift);
}
.method input { margin-bottom: 0.35rem; width: auto; }
.method .mkey { font-family: var(--font-heading); font-weight: 600; }
.method .mcount {
  font-size: var(--t-hero); font-family: var(--font-heading); font-weight: 700;
  line-height: 1.05; font-variant-numeric: tabular-nums;
}
/* "pieces" under the count, not beside it. */
.method .mcount + .small {
  font-size: var(--t-caption); text-transform: uppercase; letter-spacing: 0.06em;
}
.method .note { margin-top: 0.35rem; }

.sqft-form { display: flex; gap: 0.35rem; align-items: center; flex-wrap: wrap; }
.sqft-form input { width: 9rem; }

/* ---------- quote ---------- */

.override-form { display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-end; margin-top: 0.75rem; }
.override-form .field { display: flex; flex-direction: column; }
.override-form .field.grow { flex: 1 1 16rem; }
.override-form .field.grow input { width: 100%; }
.override-form select, .override-form input[type=number] { width: 13rem; }

.note-override { color: var(--ochre); font-weight: 500; }
.totals .grand th, .totals .grand td { border-top: 2px solid var(--text); font-size: var(--t-lead); }
tfoot th { border-top: 1px solid var(--text); }

/* ---------- selections ---------- */

/* Every disclosure summary is a target: 21-28px before, 44px now, from
   padding. The heading inside keeps its own size. */
summary {
  min-height: 44px;
  /* Padding, NOT display:flex. Flex on a <summary> drops the disclosure
     triangle in WebKit and Blink, because the marker needs display:list-item --
     which would have quietly removed the triangle from every disclosure in the
     app on the exact browser this pass is for. */
  padding-top: var(--s-2);
  padding-bottom: var(--s-2);
  box-sizing: border-box;
}
details.card > summary { cursor: pointer; list-style: revert; }
details.card > summary h3 { display: inline-block; margin: 0 0 0.25rem; }

.fieldgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(255px, 1fr)); gap: var(--s-4); margin-top: var(--s-3); }
.fielditem { display: flex; flex-direction: column; gap: 0.2rem; padding-left: 0.6rem; border-left: 3px solid var(--rule); }
.fielditem.unset { border-left-color: var(--ochre); }
.fielditem.wide { grid-column: 1 / -1; }
.fielditem > label:first-child { font-weight: 600; font-size: var(--t-small); }

.tristate { display: flex; gap: 0.75rem; flex-wrap: wrap; }
/* The wrapper is the target, not the 13px control inside it. Extra vertical
   padding here is fine; the text stays --t-small. */
label.radio {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-weight: 400;
  font-size: var(--t-small);
  margin: 0;
  min-height: 44px;
  padding: var(--s-1) 0;
  cursor: pointer;
}
/* 13px is a mouse-sized control. 22px is still not the target -- the label is
   -- but it stops the dot being the hard part. */
label.radio input { width: auto; margin: 0; min-width: 22px; min-height: 22px; }

.doorlist { display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.3rem; }
.rowlist { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.3rem; }
.rowlist input { width: 7rem; }
em.suggested { color: var(--ochre); font-style: normal; font-size: var(--t-caption); text-transform: uppercase; letter-spacing: 0.06em; }

form.stacked .field { margin-bottom: 0.85rem; max-width: 26rem; }
input[type="file"] { font-family: var(--font-body); font-size: var(--t-small); }

/* An inapplicable field: visibly not-for-you rather than an empty box that
   invites a value. Deliberately NOT filled with a 0 -- the underlying value
   stays None, which is a different thing from a confirmed $0. */
.fielditem.inapplicable > label:first-child { color: var(--muted); }
.fielditem.inapplicable { border-left-style: dashed; border-left-color: var(--rule); }
input:disabled, select:disabled {
  background: var(--surface);
  color: var(--muted);
  border-style: dashed;
  cursor: not-allowed;
}

/* A button that is switched off has to read as switched off -- otherwise it
   reads as a working control that silently does nothing. */
button:disabled, button.secondary:disabled {
  background: var(--surface);
  color: var(--muted);
  border: 1px dashed var(--rule);
  cursor: not-allowed;
}
button:disabled:hover, button.secondary:disabled:hover {
  background: var(--surface);
  color: var(--muted);
}

/* One row per real entry, added and removed one at a time. */
.repeater { display: flex; flex-direction: column; gap: 0.4rem; margin: 0.4rem 0 0.6rem; }
.repeater-row { display: flex; align-items: center; gap: 0.5rem; }
.repeater-row label { min-width: 10.5rem; }
.repeater-row input { width: 6.5rem; }
/* A control, not a banner: the surrounding .fielditem is a flex column,
   which would otherwise stretch the button across the whole card. */
.repeater-add { align-self: flex-start; }

/* A flagged line item's badge is a way to the screen that resolves it, not
   just a label for the problem. */
.badge.resolvable { text-decoration: none; border-bottom: 1px dotted currentColor; }

.badge.resolvable:hover { background: var(--surface); }

/* ---------- price and gross margin (Randy's screen only) ---------- */
.price-form { display: flex; gap: 1rem; align-items: flex-end; flex-wrap: wrap; margin-bottom: 1rem; }
.price-form .field { display: flex; flex-direction: column; }
.price-form input { width: 12rem; font-size: var(--t-h3); }
.pricing table.margin { max-width: 26rem; }
.pricing table.margin .grand th, .pricing table.margin .grand td {
  border-top: 2px solid var(--text);
  font-size: var(--t-lead);
}
.pricing table.margin .grand strong { font-family: var(--font-heading); }
.margin-negative { color: var(--alert); }

/* ---------- 3.4 selections ---------- */

/* A count in the section heading, so a collapsed section still says whether
   there is anything left in it. */
.section-count {
  font-size: var(--t-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--ochre);
  margin-left: var(--s-2);
}

/* Reference, not work: closed, quiet, and at the bottom. */
details.card#confirmed-elsewhere > summary { color: var(--muted); }

/* ---------- 3.3 the parsed upload ---------- */

.parsed-address { margin-bottom: var(--s-4); }

/* Four across, in the shape of the Areas box on his own siding page. */
.area-figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
  gap: var(--s-4);
  margin: 0 0 var(--s-5);
  padding-bottom: var(--s-4);
  border-bottom: 1px solid var(--rule);
}
.area-figure dt {
  font-size: var(--t-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--muted);
  margin-bottom: var(--s-1);
}
.area-figure dd { margin: 0; display: flex; align-items: baseline; gap: var(--s-1); }
.area-figure .figure-medium { display: inline; font-size: var(--t-h2); color: var(--text); }
.area-unit { font-size: var(--t-caption); color: var(--muted); }

/* ---------- 3.8 the customer document's schedule ---------- */

/* His schedule is prose in a letter, so it sets like prose. Each sentence on
   its own line reads as a list without being one. */
.doc-schedule { margin: 0 0 var(--s-4); line-height: 1.6; }
.doc-schedule strong { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Operator-only, above the document and dropped in print. */
.unpriced-scope { margin: var(--s-4) 0; }

/* ---------- 3.1 the home dashboard ---------- */

/* The eight steps as a strip. Each one says where it is; the next one to do is
   the only one that raises its voice. */
.progress-strip {
  list-style: none;
  margin: var(--s-4) 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: var(--s-2);
}
.progress-step a {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  padding: var(--s-3);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  text-decoration: none;
  color: var(--muted);
  background: var(--card);
  transition: border-color var(--motion), box-shadow var(--motion);
}
.progress-step a:hover { border-color: var(--evergreen); }
.step-number {
  font-size: var(--t-caption);
  font-weight: 600;
  letter-spacing: 0.06em;
}
.step-label { font-size: var(--t-body); color: var(--text); }
.step-outstanding { font-size: var(--t-caption); color: var(--ochre); font-weight: 600; }
.step-outstanding.done { color: var(--evergreen); }

.progress-step.done a { border-color: var(--evergreen); }
.progress-step.outstanding a { border-left: 3px solid var(--ochre); }
/* The one step to do next, and the only one treated as a call to action. */
.progress-step.is-next a {
  border-color: var(--evergreen);
  border-width: 2px;
  box-shadow: var(--shadow-card);
}
.progress-step.is-next .step-label { font-weight: 600; }

.next-step { margin: var(--s-5) 0 0; }

/* The four figures, a preview of the quote screen's price card. Deliberately
   smaller than that card's -- this is a glance, not the decision. */
.home-figures {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-5) var(--s-7);
  margin: var(--s-4) 0 0;
}
.home-figure dt {
  font-size: var(--t-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--muted);
  margin-bottom: var(--s-1);
  /* Same reason as .big-figure dt: the Total cost label names what is in the
     number, so it wraps rather than stretching the row. */
  max-width: 12rem;
}
.home-figure dd { margin: 0; }
.figure-medium {
  display: block;
  font-family: var(--font-heading);
  font-size: var(--t-h1);
  font-weight: 700;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
  color: var(--evergreen);
}
/* Same colour rule as the quote screen's own figures, so the preview and the
   thing it previews agree on which number is the cost. */
.figure-medium.is-cost { color: var(--ochre); }
/* The negative treatment was only ever written for .figure-hero, so a home
   screen showing a gross profit below zero rendered it in evergreen -- the
   colour that means "this is fine". */
.figure-medium.margin-negative { color: var(--alert); }

/* ---------- 3.5 window review, Tier 2 ---------- */

/* The progress card says the count and the next one, nothing else. */
.checklist-next { font-size: var(--t-lead); margin: var(--s-3) 0 var(--s-2); }

/* A confirmed group, collapsed. It has already sunk to the bottom of the list,
   so it needs to carry its numbers and a way back, and no more. */
.confirmed-line {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s-4);
  padding-top: var(--s-3);
  border-top: 1px solid var(--rule);
}
.confirmed-dims { font-variant-numeric: tabular-nums; }
.confirmed-perimeter {
  font-family: var(--font-heading);
  font-size: var(--t-h3);
  font-variant-numeric: tabular-nums;
}
/* A done row recedes: no shadow, and the page background rather than card
   white, so the unconfirmed cards above it are what catch the eye. A class on
   the card rather than :has(), which is too new to rely on. */
.card.group.is-confirmed {
  box-shadow: none;
  background: var(--bg);
}

/* The flagged callout is a disclosure now: the decision in the summary, the
   reasoning behind it. Kept as a callout, so it still reads as a warning. */
details.callout.suspect > summary { cursor: pointer; }
details.callout.suspect > summary::marker { color: var(--alert); }
details.callout.suspect > p { margin: var(--s-3) 0 0; }

/* ---------- 3.4 the signed-estimate fill (demo helper) ---------- */

/* What the button just did, named field by field. Long on purpose: the whole
   claim it is making is "these thirty answers, and nothing else", and a
   summary that did not list them would be asking Randy to take that on
   trust. */
.filled-note { margin: 0; }
#signed-estimate .sysnote { margin: var(--s-3) 0 0; }

/* ---------- 3.7 Randy's summary sheet ---------- */

/* Block 1. His one-page sheet: buckets down the left, amounts down the right,
   then the three figures. Narrower than the full card width on purpose -- a
   money column that runs the whole page stops reading as a column. */
.sheet-buckets { max-width: 34rem; }
.sheet-buckets th { text-transform: none; letter-spacing: 0; font-size: var(--t-body);
                    font-weight: 400; color: var(--text); }
.sheet-buckets td.num { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* "Trim and other": a group heading, not a bucket, so it is set apart from the
   nine named rows above rather than looking like a tenth. */
.sheet-remainder-head th {
  padding-top: var(--s-5);
  font-size: var(--t-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--muted);
  border-bottom-color: var(--text);
}
.sheet-remainder th { padding-left: var(--s-4); color: var(--muted); }

/* One figure now, not three (rehearsal, 2026-09-22). Gross profit and the bid
   moved to the price card, where the controls that move them are; the sheet
   keeps the one number it is a breakdown of. Left as a flex row because it is
   still a dl of figures and a second bucket-level figure would belong here. */
.sheet-figures {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-5) var(--s-7);
  align-items: flex-end;
  margin: var(--s-6) 0 var(--s-4);
  padding-top: var(--s-5);
  border-top: 2px solid var(--text);
}
.sheet-figure dt {
  font-size: var(--t-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--muted);
  margin-bottom: var(--s-1);
}
.sheet-figure dd { margin: 0; }
.figure-unset {
  display: block;
  font-size: var(--t-h2);
  font-family: var(--font-heading);
  color: var(--muted);
}

/* ---------- 3.7 the price control ---------- */

/* Block 2. Three linked controls: the bid he types, the gross profit it
   implies, and a margin slider. Only the bid is saved. */
.price-control {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-5);
  align-items: flex-end;
  margin-bottom: var(--s-4);
}
.price-control .field { display: flex; flex-direction: column; }
.price-control .actions { gap: var(--s-1); }

/* The bid is where he types the number in his head, so it is the largest
   input in the app. */
.bid-input {
  font-family: var(--font-heading);
  font-size: var(--t-h1);
  font-weight: 700;
  line-height: 1.05;
  width: 11rem;
  font-variant-numeric: tabular-nums;
}
.linked-input {
  font-size: var(--t-h3);
  width: 9rem;
  font-variant-numeric: tabular-nums;
}

.margin-field { flex: 1 1 16rem; }
.margin-control { display: flex; align-items: center; gap: var(--s-3); }
.margin-control input[type="range"] { flex: 1 1 auto; width: auto; accent-color: var(--evergreen); }
.margin-readout {
  font-family: var(--font-heading);
  font-size: var(--t-h3);
  font-variant-numeric: tabular-nums;
  min-width: 4.5rem;
  text-align: right;
}

/* ---------- 3.7 the four big figures ---------- */

/* Bid, Total cost, Gross profit and Gross profit %, under the definition line
   and above the "no bid set" callout. They replace the one-line preview that
   used to sit under the inputs, which said the same thing in small grey text
   under the controls that changed it -- the wrong size and the wrong place for
   the numbers Randy is actually deciding between.

   All four update live from /static/price_figures.js as he types or drags.
   Nothing here computes them. */
.big-figures-row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s-3) var(--s-5);
  margin: var(--s-5) 0;
  padding-top: var(--s-5);
  border-top: 2px solid var(--text);
}
.big-figures {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-5) var(--s-7);
  margin: 0;
  flex: 1 1 auto;
}
.big-figure dt {
  font-size: var(--t-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--muted);
  margin-bottom: var(--s-1);
  /* The Total cost label names what is in the number and so runs long. Held to
     the width of the figure above it rather than pushing the row apart. */
  max-width: 14rem;
}
.big-figure dd { margin: 0; }

/* The live figures have moved off the saved bid. Small on purpose: it is a
   qualifier on the row, not a warning about it -- nothing is wrong, he just
   has not pressed save. It disappears the moment he does. */
.unsaved-marker {
  font-size: var(--t-small);
  font-weight: 600;
  color: var(--ochre);
  white-space: nowrap;
}

/* ---------- 3.7 the payment schedule ---------- */

/* Block 3. Rendered exactly as price_figures() returns them. */
.payments { list-style: none; margin: 0; padding: 0; max-width: 34rem; }
.payments li {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  padding: var(--s-3) 0;
  border-bottom: 1px solid var(--rule);
}
.payments li:last-child { border-bottom: none; }
.payment-number {
  flex: 0 0 auto;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  background: var(--surface);
  color: var(--muted);
  font-size: var(--t-caption);
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.payment-due { flex: 1 1 auto; }
.payment-amount {
  flex: 0 0 auto;
  font-family: var(--font-heading);
  font-size: var(--t-h3);
  font-variant-numeric: tabular-nums;
}

/* ---------- 3.7 trim comparison ---------- */

.trim-difference { font-size: var(--t-lead); margin: var(--s-4) 0; }

/* ---------- 3.7 questions for Randy ---------- */

/* The demo uses this card to ask him in person, so it sits above the detail
   and carries the lift. */
.card.questions { box-shadow: var(--shadow-lift); border-color: var(--ochre); }

/* The itemized detail begins here, below everything he actually decides. */
.detail-head { margin-top: var(--s-7); }

/* ---------- 2.5 hero numbers ---------- */

/* The display treatment, at one size. The method piece counts (.mcount above)
   and the five money figures on the quote screen -- the sheet's Total cost,
   and Bid / Total cost / Gross profit / Gross profit % in the price card.
   Restraint is the point -- if everything is big, nothing is.

   ONE SIZE for all four in the price card (rehearsal, 2026-09-22). They are
   four readings of one decision; making one of them bigger would claim it
   mattered more, and which one that is changes with the question Randy is
   asking. The bid used to be alone at --t-bid for exactly the opposite
   reason, and beside the other three it read as the answer rather than as one
   of them. */
.figure-hero {
  display: block;
  font-family: var(--font-heading);
  font-weight: 700;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
  font-size: var(--t-hero);
  color: var(--evergreen);
}

/* Colour carries the one distinction that is real: three of these move when
   Randy types, and the cost does not. Evergreen for the three, the ochre
   accent for the cost.

   Undarkened ochre, checked rather than assumed: #CA7E31 on the card white
   measures 3.21:1, which clears WCAG's 3:1 for text at this size (--t-hero,
   well past the 24px large-text threshold). If it is ever reused below
   --t-h2 it will need a darker token -- it fails 4.5:1 comfortably. */
.figure-hero.is-cost { color: var(--ochre); }
.figure-hero.margin-negative { color: var(--alert); }

/* ---------- elevation image viewer ---------- */

/* Present on every window-group row, including one no extraction has touched:
   the page is what a width is read off, so the way to it is never conditional
   on a value already existing. */
.viewer { margin: 0.75rem 0; }
.viewer-body { margin-top: 0.6rem; }
.viewer-bar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-bottom: 0.5rem;
}
.viewer-bar select { width: auto; margin: 0; }
.viewer-provenance { flex: 1 1 14rem; }
.viewer-zoom { display: inline-flex; align-items: center; gap: 0.3rem; }
.viewer-zoom button {
  padding: var(--s-1) var(--s-2);
  font-size: var(--t-small);
  line-height: 1.2;
  /* Square, and big enough to hit while holding a tablet. 27x27 before. */
  min-width: 44px;
  min-height: 44px;
}
.zoom-level {
  font-variant-numeric: tabular-nums;
  font-size: var(--t-small);
  color: var(--muted);
  min-width: 3.2rem;
  text-align: center;
}
.viewer-pane {
  max-height: 70vh;
  overflow: auto;
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--radius-card);
}
.viewer-pane img { display: block; width: 100%; height: auto; cursor: zoom-in; }
.viewer-pane.zoomed img { cursor: zoom-in; }

/* ---------- customer-facing document ---------- */

/* The document proper: what the customer receives. Everything around it is
   Randy's scaffolding and is dropped when this page prints. */
.document {
  position: relative;
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--radius-card);
  padding: var(--s-7) 2.75rem var(--s-6);
  max-width: 48rem;
  margin: 0 auto var(--s-5);
  box-shadow: var(--shadow-card);
}

/* Randy watermarks his own estimates with the logo. Roughly page-width,
   centred, faint enough to read straight through. Kept at z-index 0 with the
   text at 1 rather than a negative z-index, which some print renderers drop. */
.watermark {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 88%;
  height: auto;
  opacity: 0.2;
  z-index: 0;
  pointer-events: none;
  user-select: none;
}
.document-body { position: relative; z-index: 1; }

.letterhead { text-align: center; }
.letterhead img.doc-logo { height: 78px; width: auto; display: inline-block; }

.doc-date { margin: 2rem 0 1.5rem; }
.doc-customer { margin: 0 0 1.75rem; line-height: 1.5; }

.doc-title {
  text-align: center;
  font-size: var(--t-h3);
  margin-bottom: var(--s-4);
}
.doc-lede { font-weight: 600; margin-bottom: 0.9rem; }

.doc-section {
  font-size: var(--t-body);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 0.3rem;
  margin: 2rem 0 0.9rem;
}

.scope { margin: 0; padding-left: 1.6rem; }
.scope li { margin-bottom: 0.55rem; line-height: 1.5; }

/* The one dollar figure on the page, as the final numbered item. */
.scope li.scope-total {
  margin-top: 1.1rem;
  padding-top: 0.8rem;
  border-top: 2px solid var(--text);
  font-size: var(--t-lead);
}
/* The one deliberate literal size in this file. Section 2.5: "On the customer
   copy, the total price stays at its current 1.3rem inside the document. That
   page mimics a printed letter and must not shout." No scale step sits here,
   and promoting it to --t-h2 would shout. */
.scope li.scope-total strong { font-family: var(--font-heading); font-size: 1.3rem; }

/* An estimate with no price still prints -- saying so, loudly, so a priceless
   document can't be handed over by mistake. */
.no-price-notice {
  margin-top: 1.2rem;
  padding: 0.6rem 0.8rem;
  border: 1px dashed var(--alert);
  color: var(--alert);
  font-weight: 600;
}

/* Heading with nothing under it, on purpose: the rule that derives the
   milestone amounts is still an open question with Randy, and an invented
   schedule on a customer document is worse than an empty one. */
.schedule-pending { min-height: 4.5rem; }

.signatures {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.4rem 3rem;
  margin-top: 3rem;
}
.sigline { display: flex; flex-direction: column; gap: var(--s-1); font-size: var(--t-body); }
.sigline .rule { border-bottom: 1px solid var(--text); height: 2.5rem; }

/* The layout table whose <tfoot> carries the footer onto every printed page.
   Styled back to invisibility: it is a print mechanism, not a table. */
.doc-sheet { width: 100%; border-collapse: collapse; position: relative; z-index: 1; }
.doc-sheet > tbody > tr > td,
.doc-sheet > tfoot > tr > td { padding: 0; border: none; }
.doc-sheet > tfoot { border: none; }

/* Randy's own footer block, on every page of his document. */
.doc-footer {
  margin-top: 2.5rem;
  padding-top: 1rem;
  text-align: center;
  font-size: var(--t-caption);
  font-weight: 600;
  line-height: 1.5;
  color: var(--ochre);
}

@media print {
  /* Sticky is a screen behaviour. Static again here, and hidden as before --
     stated explicitly because position:sticky on a printed element is the kind
     of thing that repeats a header onto every page in some renderers. */
  .site-header { position: static !important; }
  .site-header, .screen-only { display: none !important; }
  body { background: #fff; }

  /* Paper does not have elevation or motion. Section 2.3. */
  * { box-shadow: none !important; transition: none !important; animation: none !important; }
  main { padding: 0; margin: 0; max-width: none; }
  .document { border: none; border-radius: 0; padding: 0; max-width: none; margin: 0; box-shadow: none; }

  /* position:fixed repeats an element on every printed page, which is how
     both the watermark and the footer get onto all of them. */
  .watermark {
    position: fixed;
    top: 50%;
    left: 50%;
    width: 80%;
  }
  /* The <tfoot> repeats and reserves its own space, so the footer needs no
     positioning at all -- only a little breathing room above it. */
  .doc-footer { margin-top: 0; padding-top: 0.5rem; }

  .doc-section, .scope li, .signatures, .no-price-notice { break-inside: avoid; }
  @page { margin: 0.7in 0.8in 0.6in; }
}

/* ---------- the one breakpoint ---------- */

/* 900px, and the only width-based media query in the app. Everything else
   reflows on its own through flex-wrap and auto-fit grids; a width audit at
   768 and 1024 found no horizontal overflow and no table scrolling sideways,
   so this handles the two things that reflow badly rather than not at all. */
@media (max-width: 900px) {
  /* The step nav used to switch from wrapping to scrolling here. It now
     scrolls at every width (see .stepnav), so it needs nothing from this
     breakpoint. */

  /* Four method cards, two by two. auto-fit gave three across and one alone
     below, which reads as a fourth option that did not fit rather than as
     four equals. */
  .methods { grid-template-columns: repeat(2, 1fr); }

  /* Below 900px -- the smaller iPads in portrait -- the tool label and its
     divider give their room to the customer name. The design target, the
     larger iPad Pro, is 1024px or wider in portrait and keeps it. */
  .tool-divider, .tool-name { display: none; }
}

/* ---------- price list ---------- */

.pricelist td, .pricelist th { vertical-align: top; }
.pricelist td.num { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* A rate that is recorded but that nothing reads yet: present so Randy can see
   the number exists, dimmed so it doesn't read as part of a quote. */
.pricelist tr.not-in-use td { color: var(--muted); }
.badge.unused {
  color: var(--muted);
  border: 1px dashed var(--rule);
  background: none;
}

/* The explicit "confirmed: none here" answer. Set apart from the entries above
   it, because it is the opposite of them rather than another one. */
.none-of-these {
  margin-top: 0.5rem;
  padding-top: 0.5rem;
  border-top: 1px dotted var(--rule);
}

/* A field the user was just sent to, or that blocked the step they tried to
   finish. Deliberately louder than the ochre "not yet answered" left border:
   ochre means "still to do", red means "this one, now". */
.fielditem.flagged {
  border-left-color: var(--alert);
  border-left-width: 4px;
  background: var(--alert-wash);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 0.4rem 0.6rem;
}
.fielditem.flagged > label:first-child { color: var(--alert); }
.flag-message { color: var(--alert); font-weight: 500; }

.missing-list { margin: 0.4rem 0 0; padding-left: 1.1rem; }
.missing-list li { margin-bottom: 0.2rem; }
a.jump-to-field { font-weight: 600; }

/* A window group row the reviewer was just sent to. */
.card.group.flagged {
  border-color: var(--alert);
  border-width: 2px;
  box-shadow: 0 0 0 3px var(--alert-wash);
}

/* Required, said once, quietly. */
.required-mark {
  font-weight: 400;
  font-size: var(--t-caption);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  margin-left: 0.35rem;
}
form.stacked .field.flagged {
  border-left: 4px solid var(--alert);
  background: var(--alert-wash);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 0.4rem 0.6rem;
}
form.stacked .field.flagged label { color: var(--alert); }

/* Read before the demo rather than on screen just now. Said quietly, because
   it matters that it is true and not that it is loud. */
.precomputed-note { font-style: italic; }

/* ---------- 2.6 the note system ---------- */

/* Two kinds of explanatory text that currently look identical.

   A .rule records something from Randy's own pages or a confirmed answer from
   him -- "Level starter / 12, round up, no waste factor". These are the
   evidence that the tool was built from his process, so they are visible and
   attributed.

   A .sysnote explains how the software derived something or what a field
   gates. Those stay quiet.

   The test for which is which: if the sentence could have come out of Randy's
   mouth, it is a .rule. When in doubt it is a .sysnote -- a "Randy's rule"
   label on something he never said would undermine the whole device. */
.rule {
  border-left: 3px solid var(--ochre);
  background: var(--ochre-wash);
  padding: var(--s-2) var(--s-3);
  border-radius: 0 var(--radius) var(--radius) 0;
  font-size: var(--t-small);
  color: var(--text);
}

.rule::before {
  content: "Randy's rule";
  display: block;
  font-size: var(--t-caption);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ochre);
  margin-bottom: var(--s-1);
}

/* A note about the software. No border, no label, no volume. */
.sysnote { font-size: var(--t-small); color: var(--muted); }

/* Either kind, collapsed when it runs long. The summary reads "Why" so the
   field stays scannable and the note stays one click away. */
details.note > summary {
  font-size: var(--t-small);
  color: var(--muted);
  cursor: pointer;
}
details.note > summary::marker { color: var(--rule); }
details.note[open] > summary { margin-bottom: var(--s-1); }

/* ---------- 2.8 step navigation states ---------- */

/* The header nav is the app's spine. Tier 1 ships the treatments; section 2.8
   T2 wires the per-step state that selects between them. */

/* Done: evergreen, with a check before the label. */
.stepnav a.done { color: var(--evergreen); }
.stepnav a.done .step-name::before { content: "\2713\00a0"; }

/* Outstanding items: an ochre dot after the step name, and the count in words
   on the line below it. */
.stepnav a.outstanding .step-name::after {
  content: "";
  display: inline-block;
  width: 0.4em;
  height: 0.4em;
  margin-left: 0.35em;
  vertical-align: 0.15em;
  border-radius: 50%;
  background: var(--ochre);
}

/* Not started: the default muted treatment, said explicitly so the four
   states read as one set. */
.stepnav a.todo { color: var(--muted); }

/* Current: the step you are on looks settled rather than tappable -- weight
   and an underline, no fill and no border. It is the one thing in the nav you
   cannot act on, so it should not be the loudest. After the three states
   above so it wins over their colours; a done step keeps its tick. */
.stepnav a[aria-current="page"] {
  color: var(--text);
  font-weight: 700;
  border-color: transparent;
  background: transparent;
}
.stepnav a[aria-current="page"] .step-name {
  padding-bottom: 2px;
  box-shadow: inset 0 -3px 0 var(--evergreen);
}

/* The count in words, under the step name. Caption size, which is the floor
   of the type scale, not something smaller; quieter than the name through
   case and weight, not through fading it. */
.step-count {
  font-size: var(--t-caption);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}

/* The current step drops the dot -- it is the screen you are on, so what is
   outstanding is already in front of you. The count words stay. */
.stepnav a[aria-current="page"] .step-name::after { display: none; }

/* Per-door brick surround: one row per confirmed garage opening. */
.surround-list { margin-top: 0.6rem; display: flex; flex-direction: column; gap: 0.3rem; }
.surround-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.surround-row > span:first-child { min-width: 3rem; font-weight: 600; }

/* The payment schedule on the customer copy. */
.payments { margin: 0; padding-left: 1.6rem; }
.payments li {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.45rem;
  line-height: 1.5;
}
.payments .payment-amount { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ---------- saving as Randy goes ---------- */
/* Quiet when all is well; the alert colour only when an answer is not saved,
   because that is the one thing he must not miss. */
.save-status { min-height: 1.4em; margin: 0 0 var(--s-3); }
.save-status[data-kind="retrying"],
.save-status[data-kind="stale"],
.save-status[data-kind="invalid"] { color: var(--alert); font-weight: 600; }
.site-header .saved-at { white-space: nowrap; }
.site-header .all-quotes { white-space: nowrap; flex: 0 0 auto; }
.quote-list td, .quote-list th { vertical-align: middle; }

/* ---------- the quote list's row buttons ---------- */
/* What needs Randy stands out; what does not stays quiet. Every one is still
   a 44px .btn. */
/* Draft (row-work): work to pick up. The default filled primary; no rule needed. */
/* Finalized (standing in for sent): a customer who has not answered is money
   waiting on a follow-up call. Ochre, which the app already uses for "this
   needs you". Dark text: white on this ochre is 3.2:1, below 4.5:1. */
.quote-list .btn.row-waiting { background: var(--ochre); border-color: var(--ochre); color: var(--text); }
.quote-list .btn.row-waiting:hover { background: var(--ochre); filter: brightness(0.95); }
/* Won: reference while the job runs. */
.quote-list .btn.row-won { background: var(--card); border-color: var(--rule); color: var(--evergreen); }
.quote-list .btn.row-won:hover { background: var(--surface); }
/* Lost: history. */
.quote-list .btn.row-lost { background: var(--card); border-color: var(--rule); color: var(--muted); }
.quote-list .btn.row-lost:hover { background: var(--surface); }

/* On the list, All quotes is where you are: plain text, not a button. */
.site-header .all-quotes.here { font-weight: 600; color: var(--text); white-space: nowrap; flex: 0 0 auto; }
.site-header .back-to { white-space: nowrap; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; gap: 0.4em; }
/* The words, not the button, take the ellipsis, so the arrow is never cut. */
.back-to-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Upload: the file chooser is the whole width of the card and at least
   44px tall, well apart from Parse export, and shows the chosen file. With no
   JavaScript the native control shows below it and works as it always did;
   with it (.enhanced) the native control is hidden but still focusable, and
   the label is the only target. */
.file-target {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--s-1);
  width: 100%;
  box-sizing: border-box;
  min-height: 44px;
  padding: var(--s-5) var(--s-4);
  border: 2px dashed var(--rule);
  border-radius: var(--radius);
  background: var(--card);
  cursor: pointer;
  text-align: center;
}
.file-target:hover { border-color: var(--evergreen); }
.file-target-title { font-weight: 600; color: var(--evergreen); }
.file-chosen { color: var(--muted); overflow-wrap: anywhere; }
.upload-form.enhanced input[type="file"] {
  position: absolute; width: 1px; height: 1px; opacity: 0; overflow: hidden;
}
.upload-form.enhanced:has(input[type="file"]:focus-visible) .file-target {
  border-color: var(--evergreen);
  outline: 2px solid var(--evergreen);
}
/* Without the script nothing would update it, and the native control says
   which file is chosen by itself. */
.upload-form:not(.enhanced) .file-chosen { display: none; }
.upload-form input[type="file"] { margin-top: var(--s-3); min-height: 44px; width: 100%; }
.upload-actions { margin-top: var(--s-6); }
/* The address question's two answers: whole-width targets for a finger,
   one above the other, so a long address in a button wraps rather than
   squeezing its neighbour. */
.address-choice {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  margin-top: var(--s-5);
}
.address-choice-button {
  width: 100%;
  min-height: 44px;
  white-space: normal;
  text-align: center;
}
.upload-error { color: var(--alert); font-weight: 600; }

/* Under every page: the one way to tell Jon something went wrong. Quiet,
   because it is needed rarely, but a whole finger's height to tap. */
.site-footer {
  max-width: 1120px;
  margin: var(--s-6) auto var(--s-5);
  padding: 0 var(--s-4);
  text-align: center;
  font-size: var(--t-small);
}
.site-footer a {
  display: inline-block;
  min-height: 44px;
  line-height: 44px;
  color: var(--muted);
}
