/* Guest Haus Launch Control — internal register.
 *
 * Tokens transcribed from design-reference/tokens.md. Declared once here as
 * custom properties so every later screen reads from the same values rather
 * than repeating hex codes, which is how a palette drifts.
 *
 * The owner register (sage background, serif body, larger radii) lands in
 * Phase 7 and gets its own block rather than overriding these.
 */

:root {
  /* Brand */
  --ink: #241B12;
  --rust: #54281A;
  --forest: #2E382D;
  --sage: #9BA592;
  --clay: #BF9C73;
  --cream: #E3D6C1;
  /* No gold. The palette has six colours; a seventh arrived as the focus ring
   * and two markers and survived as "the exception". Focus rings and both
   * markers use --rust, which also passes 3:1 on every surface here — gold
   * managed 1.99 against a strong border, and tan would have been worse. */

  /* Internal surfaces */
  --bg: #F5EFE2;
  --surface: #FFFDF6;
  --surface-raised: #FBF7EC;
  --surface-tint: #F1E9D4;
  --border: #E6DCC4;
  --border-soft: #EFE6D0;
  --border-strong: #D8CDB4;

  /* Text */
  --text: #241B12;
  --text-secondary: #7A6E58;
  --text-muted: #8A7E68;
  --text-faint: #A79B85;

  --r-sm: 5px;
  --r-md: 7px;
  --r-lg: 8px;
  --r-xl: 10px;
  --r-pill: 999px;

  --gutter: 28px;
  --content-max: 1180px;

  /* The header's content height, without its 1px bottom border. The crumb
   * bar's sticky offset is calculated from this rather than repeating the
   * number, because the two drifted apart once already: the bar rendered
   * 77px on a phone while the crumb bar stayed pinned at 57px and hid
   * underneath it. */
  --header-height: 56px;

  /* The crumb bar's content height, without its 1px bottom border. Anything
   * pinned below both bars reads this and --header-height rather than adding
   * the numbers up itself — the bulk bar hard-coded top: 8px and, once pinned,
   * sat underneath them both. */
  --crumb-height: 36px;
}

* { box-sizing: border-box; }

/* `hidden` is only `display: none` at user-agent specificity, so any class
 * that sets a display — .save-status, .add-row — silently outranks it and the
 * element stays on screen. Caught twice by looking at the page; this is the
 * fix that stops it happening a third time. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: Rubik, system-ui, -apple-system, sans-serif;
  font-size: 14px;
  line-height: 1.5;
}

body.centered {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

h1 {
  font-family: 'Libre Baskerville', Georgia, serif;
  font-weight: 400;
  font-size: 30px;
  margin: 0 0 20px;
}

a { color: var(--rust); text-decoration: none; }
a:hover { color: var(--forest); text-decoration: underline; }
::selection { background: var(--cream); }

/* Flat and print-like: the drawer is the only shadowed element in the whole
 * design, and it does not exist yet. */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  padding: 24px 28px;
}

/* Application header — chrome-spec §1.
 *
 * Full-bleed bar, contained contents: the background and the bottom border run
 * to the viewport edges while the lockup's left edge and Sign out's right edge
 * line up with the page content below, because both use the same container. */
.appbar {
  position: sticky;
  top: 0;
  z-index: 20;
  background: #FBF7EC;
  border-bottom: 1px solid #E3D9C2;
}
.appbar-inner {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: var(--header-height);   /* border-box: the whole row, not its contents */
  max-width: 1180px;
  margin: 0 auto;
  padding: 10px 28px;
}

/* Lockup — stacked, one link, the only serif in the header. The 28px logo
 * slot goes to the left of this when an asset ships; there is none in the
 * build today, so the lockup is the two text lines alone. */
.lockup {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 10px;
  /* Never shrinks. It had `min-width: 0`, which let flex squeeze it below its
   * own content — 130px of wordmark into a 74px box — and `overflow: visible`
   * then painted the surplus underneath the nav pill, so the header read
   * "Launch Contr". The width was a symptom; being shrinkable was the fault.
   * There is no width at which squeezing the brand mark is the right answer,
   * so this holds at every viewport, not just below 640px. */
  flex: none;
  text-decoration: none;
}
.lockup:hover,
.lockup:active { text-decoration: none; }
/* The brand mark, at the 50px width the guidelines set as its minimum. The
 * bar is 56px, so it nearly fills the height — there is no size at which both
 * that minimum and the 10mm clear space the guidelines ask for fit in a bar
 * this tall, and the minimum is the one that protects legibility. */
.lockup-mark { display: block; width: 50px; height: auto; flex: none; }

/* The two lines, stacked beside the mark rather than under it. */
.lockup-text { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; }

.lockup-name {
  font-family: 'Libre Baskerville', Georgia, serif;
  font-weight: 400;
  font-size: 16px;
  line-height: 1.1;
  color: var(--ink);
  white-space: nowrap;
}
.lockup-org {
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 2px;
  color: #8A7E68;
  white-space: nowrap;
}

/* 28px margin, rule, 28px padding — a gap either side of the divider. */
.appbar-nav {
  display: flex;
  gap: 4px;
  margin-left: 28px;
  margin-right: auto;
  padding-left: 28px;
  border-left: 1px solid #E3D9C2;
}
.nav-item {
  padding: 7px 14px;
  border-radius: var(--r-md);
  font-size: 13px;
  font-weight: 500;
  color: #7A6E58;
  background: transparent;
  text-decoration: none;
  white-space: nowrap;
}
.nav-item:hover { background: #F1E9D6; color: var(--rust); text-decoration: none; }
.nav-item:active { background: var(--cream); color: var(--rust); }
/* Current holds its ground on hover. The spec gives current a darker fill than
 * hover, so letting hover win would read as the item dimming when you point at
 * it — the opposite of what a hover is for. */
.nav-current,
.nav-current:hover,
.nav-current:active { background: #EDE4CF; color: var(--ink); text-decoration: none; }

.appbar-user { display: flex; align-items: center; gap: 12px; }
.appbar-username { font-size: 12.5px; font-weight: 400; color: #8A7E68; }
.signout {
  padding: 0;
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 400;
  color: #8A7E68;
  background: none;
  border: none;
  text-decoration: none;
  cursor: pointer;
}
.signout:hover,
.signout:active { color: var(--rust); text-decoration: underline; }

/* The page column — ui-audit-spec §3.
 *
 * One 20px gap between everything stacked on a page, set once here rather than
 * as a margin on each card. The spec is explicit that it is a flex gap: the
 * old arrangement was a margin per card plus a different margin on the tab row
 * plus none at all in places, which is why the settings sections sat 4px apart
 * while the portfolio card sat 16px below its tabs.
 *
 * Children that carry their own margins are zeroed below rather than left to
 * add to this. */
.page {
  display: flex;
  flex-direction: column;
  gap: 20px;
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 32px var(--gutter) 80px;
}

.wordmark { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.wordmark-name {
  font-family: 'Libre Baskerville', Georgia, serif;
  font-size: 18px;
  white-space: nowrap;
}
.wordmark-org {
  font-size: 10px;
  letter-spacing: 2.5px;
  color: var(--text-muted);
  white-space: nowrap;
}

/* Auth ---------------------------------------------------------------- */

.auth { width: 100%; max-width: 380px; }
.auth .wordmark { margin-bottom: 24px; }

.field { display: block; margin-bottom: 16px; }

/* A checkbox and its text on one line, rather than the stacked label/control
   .field gives everything else.
   
   The tap target is the whole label, not the 13px box the browser draws: 44px
   tall and full width, so the text is as clickable as the tick. Same shape as
   the punch row's checkbox, which draws a small box inside a 44px hit area. */
.field-check {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  margin-bottom: 4px;
  cursor: pointer;
}
.field-check input[type="checkbox"] {
  flex: none;
  width: 18px;
  height: 18px;
  margin: 0;
  accent-color: var(--forest);
}
.field-check > span { font-size: 13.5px; color: var(--text); }

.label {
  display: block;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.5px;
  color: var(--text-faint);
  margin-bottom: 8px;          /* ui-audit-spec §4: label-gap-below */
}

/* Section labels — ui-audit-spec §4.
 *
 * The app marks its two kinds of caps label differently already: a field's own
 * label is a <span class="label"> inside the field, and a section label is a
 * <legend class="label"> opening a fieldset. Only the second takes the 20px
 * above, which is what the spec's examples all are — PULL-OUTS AND SOFA BEDS
 * sat flush against the bed count fields with no gap at all.
 *
 * Nothing precedes the first section in a panel, so it takes no gap. */
/* The 20px sits on the fieldset, not on the legend. A <legend> is laid out by
 * the browser as part of the fieldset's border box and ignores its own
 * margin-top: set there it computed as 20px and moved nothing at all. */
fieldset.field { margin-top: 20px; }

/* Only a fieldset that opens its panel has nothing above it. A plain
 * :first-child was too broad: the unit sections are rendered into empty host
 * <div>s by supply.js, so "UNIT A ONLY" and "UNITS" are each first inside
 * their host and were exempted — while on screen they sat flush against the
 * section above, at a measured gap of 0. */
.wizard-panel > fieldset.field:first-child,
.card > fieldset.field:first-child { margin-top: 0; }

/* "When a helper sentence follows the label, order is label → 8px → helper →
 * 8px → control." The hint carries the second 8px. */
legend.label + .hint { margin-bottom: 8px; }

input[type="email"],
input[type="password"],
input[type="text"],
input[type="url"],
input[type="date"] {
  width: 100%;
  padding: 9px 11px;
  font-family: inherit;
  font-size: 14px;
  color: var(--text);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
}

/* Number inputs — ui-audit-spec §4.
 *
 * These were not in the rule above, so they shipped as raw browser controls:
 * a 2px inset border, white fill, square corners, and spinner arrows, sitting
 * beside fields that had all been styled. Date inputs are deliberately left
 * native and are not touched here. */
input[type="number"] {
  width: 120px;
  padding: 8px 12px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 400;
  color: var(--ink);
  background: #FBF7EC;
  border: 1px solid #D8CDB4;
  border-radius: var(--r-md);
}
input[type="number"]::placeholder { color: #B3A78E; }
input[type="number"]:focus { border-color: var(--rust); }

/* Spinners removed rather than restyled: they cannot be styled from the
 * tokens, their arrows are below every hit-target size in this app, and the
 * values are single-keystroke 0-5 entries. type="number" and inputmode stay,
 * so the phone keypad is unaffected. */
input[type="number"] {
  appearance: textfield;
  -moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* The one number field that is not a 0-5 count. */
input[type="number"][data-field="squareFeet"] { width: 140px; }

/* chrome-spec §3: "Focus-visible for ALL variants: outline 2px solid #B58A4C,
 * outline-offset 2px, on top of the default appearance — never a color change
 * alone." Anchors are included because most of this app's buttons are links.
 *
 * The ring is --rust, not the #B58A4C the spec names, and this is a deliberate
 * deviation. That gold is not in the brand palette at all, and it is too pale
 * to be a focus indicator: measured against the surfaces it lands on it scores
 * 3.08 at best and 1.99 against a strong border, where 3:1 is the floor. Tan,
 * the nearest brand colour, is worse (2.51 to 1.62) and would have been
 * indistinguishable from the tan border on an active filter. Rust scores 7.85
 * to 12.17 everywhere and 4.85 against that tan border. */
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
button:focus-visible,
a:focus-visible {
  outline: 2px solid var(--rust);
  outline-offset: 2px;
}

/* Button variants -------------------------------------------------------- */
/*
 * Every state below is from the tables in design-reference/chrome-spec.md §3
 * and is written as a literal hex rather than a token, because a handful of
 * the spec's values are not in tokens.md (#F1E9D6, #EDE4CF, #3A4739, #E3D6C1)
 * and silently rounding them to the nearest token is the kind of substitution
 * that makes a spec unfalsifiable.
 *
 * Every variant states its own :hover explicitly. A class selector plus a
 * pseudo-class outranks the global `a:hover` (chrome-spec §4), so a button
 * that happens to be an anchor cannot inherit a text-colour-only hover — which
 * is what §3's "Rule to delete" is really about. See the note on .icon-button
 * further down for the one rule in this file that actually had that shape.
 */

.button-primary,
.button-outline,
.button-ghost,
.button-danger,
.button-danger-ghost {
  font-family: inherit;
  font-weight: 500;
  border-radius: var(--r-md);
  cursor: pointer;
}

/* Filled primary */
.button-primary {
  width: 100%;
  padding: 8px 16px;
  margin-top: 4px;
  font-size: 13px;
  color: #F3EEE0;
  background: #2E382D;
  border: 1px solid transparent;
}
.button-primary:hover {
  background: #3A4739;
  color: #F3EEE0;
  text-decoration: none;
}
.button-primary:active { background: #241B12; color: #F3EEE0; }
.button-primary:disabled,
.button-primary[aria-disabled="true"] {
  background: #EDE4CF;
  color: #A79B85;
  cursor: default;
}

/* Outline — a border, and the variant almost every control in this app uses */
.button-outline {
  padding: 7px 14px;
  font-size: 13px;
  color: #54281A;
  background: transparent;
  border: 1px solid #D8CDB4;
}
.button-outline:hover {
  background: #F1E9D6;
  color: #54281A;
  text-decoration: none;
}
.button-outline:active { background: #EDE4CF; color: #54281A; }
.button-outline:disabled,
.button-outline[aria-disabled="true"] {
  background: transparent;
  color: #B3A78E;
  border-color: #EFE6D0;
  cursor: default;
}

/* Ghost — no border, ever */
.button-ghost {
  padding: 7px 12px;
  font-size: 12.5px;
  color: #54281A;
  background: transparent;
  border: none;
}
.button-ghost:hover {
  background: #F1E9D6;
  color: #54281A;
  text-decoration: none;
}
.button-ghost:active { background: #EDE4CF; color: #54281A; }
.button-ghost:disabled,
.button-ghost[aria-disabled="true"] {
  background: transparent;
  color: #B3A78E;
  cursor: default;
}

.error {
  margin: 0 0 16px;
  padding: 9px 12px;
  font-size: 13px;
  color: var(--rust);
  background: #EAD8D1;
  border-radius: var(--r-md);
}

/* Rows ---------------------------------------------------------------- */

.row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 13px 0;
  border-bottom: 1px solid var(--border-soft);
}
.row:last-child { border-bottom: none; }
.row:first-child { padding-top: 0; }

.row-title { font-size: 14px; font-weight: 500; }
.row-meta { font-size: 12px; color: var(--text-muted); margin-top: 2px; }

.row-progress { display: flex; align-items: center; gap: 10px; }
.track {
  width: 160px;
  height: 8px;
  background: #EBE1C9;
  border-radius: 4px;
  overflow: hidden;
}
.fill { height: 100%; background: var(--rust); border-radius: 4px; }
.pct { font-size: 13px; font-weight: 600; min-width: 36px; text-align: right; }

.muted { color: var(--text-muted); }
.empty p { margin: 0 0 8px; }
.empty p:last-child { margin-bottom: 0; }

/* The prototype switched layouts on a prop; real breakpoints start here.
 * 900px is the desktop/mobile split named in the build plan. */
@media (max-width: 900px) {
  :root { --gutter: 16px; }
  .page { padding-top: 24px; }
  h1 { font-size: 26px; }
  .row { flex-direction: column; align-items: flex-start; gap: 8px; }
  .track { width: 100%; }
  .row-progress { width: 100%; }

  /* The org tag is the first thing to go: at 375px the wordmark and the
   * sign-out control together do not fit, and the wordmark is the one that
   * has to stay legible. */
  .wordmark-org { display: none; }
  /* The bar is full-bleed and its inner element carries the gutter, per
   * chrome-spec §1. This rule used to put `padding: 10px var(--gutter)` on the
   * bar itself while .appbar-inner kept its own 10px/28px, which cost two
   * things at once: the bar measured 77px instead of 56, and the effective
   * gutter became 44px a side (16 + 28), leaving 305px of content on a 393pt
   * screen rather than 361. */
  .appbar { padding: 0; }
  .appbar-inner { padding: 0 var(--gutter); }
  /* Every tappable thing on a phone clears 44px. These are used one-handed,
   * standing up, often in a hurry. */
  .tab { display: inline-flex; align-items: center; min-height: 44px; padding: 0 16px; }
  /* The wordmark links home. The sign-out button already sets the bar's
   * height, so this costs no vertical space. */
  a.wordmark { min-height: 44px; align-items: center; }
  /* 44px minimum touch target — this is used one-handed in a house.
   *
   * The display matters as much as the height. `min-height` alone on a block
   * element grows the box and leaves the label at the top of it: Settings on a
   * Portfolio card sat 12px above the meta line it is supposed to share, and
   * read as its own row. Centring is what makes a 44px target look like a
   * control rather than a misaligned one. */
  .button-ghost, .button-outline, .button-primary {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .hide-narrow { display: none; }
}

/* Portfolio, forms and settings ----------------------------------------- */

/* The single narrow variant. There were two — .page.narrow at 720px on three
 * screens and .page-narrow at 780px on one — which is the kind of pair that
 * ends with a fifth screen picking whichever it happened to copy. */
.page.narrow { max-width: 720px; }

.page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}
.page-head h1 { margin-bottom: 4px; }

.subline { margin: 0; font-size: 13px; color: var(--text-muted); }
/* Breadcrumb sub-bar — ui-audit-spec §1(b).
 *
 * Sticky under the header rather than scrolled away with the page, because the
 * screens it appears on are long: the punch list runs to ninety rows and the
 * supply sheet further, and a back link only at the top is a back link you
 * have to scroll to find. Quieter than the header (page background, softer
 * rule) so the two bars read as one piece of chrome, not two headers.
 *
 * ui-audit-spec §1(b) says top: 56px, "the header height". Taken literally
 * that parks this bar on top of the header's own 1px bottom border, which
 * chrome-spec §1 specifies in the same breath — the spec contradicts itself
 * across two files. 57px keeps both. */
.crumb-bar {
  position: sticky;
  top: calc(var(--header-height) + 1px);   /* the header plus its bottom border */
  z-index: 19;                   /* one step below the header's 20 */
  background: #F5EFE2;
  border-bottom: 1px solid #EFE6D0;
}
.crumb-bar-inner {
  display: flex;
  align-items: center;
  min-height: var(--crumb-height);
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 0 28px;
}
.crumbs {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  list-style: none;
  margin: 0;
  padding: 0;
}
.crumb-item { display: flex; align-items: center; min-width: 0; }
.crumb-link,
.crumb-current { font-size: 12.5px; font-weight: 400; }

/* One line, always, so the bar is the height --crumb-height says it is:
 * mobile-spec gives it as 36px, and the pinned bulk bar reads that number. It
 * used to wrap — a 48-character launch name made it 62px at 393pt, and the
 * pinned bar's top 17px went underneath it.
 *
 * The launch name gives way, truncated; Portfolio and the page name keep their
 * width. It is the second segment wherever there is one (partials/crumbs.ejs
 * builds Portfolio, launch, page, in that order). */
.crumbs { flex-wrap: nowrap; min-width: 0; }
.crumb-item { flex-shrink: 0; }
.crumb-item:nth-child(2) { flex-shrink: 1; }
.crumb-link,
.crumb-current { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.crumb-link { color: var(--rust); text-decoration: none; }
.crumb-link:hover { color: var(--forest); text-decoration: underline; }
.crumb-current { color: #8A7E68; }
.crumb-sep { margin: 0 8px; color: #B3A78E; }

/* The old single-line crumb, still used by screens outside a launch. */
.crumb { margin: 0 0 12px; font-size: 13px; }
.footnote { margin: 0; font-size: 12px; color: var(--text-faint); }
.hint { margin: 0; font-size: 12px; color: var(--text-muted); }

h2 {
  font-family: 'Libre Baskerville', Georgia, serif;
  font-weight: 400;
  font-size: 20px;
  margin: 0;
}

a.wordmark { text-decoration: none; color: inherit; }
a.wordmark:hover { text-decoration: none; }

.button-primary.inline { width: auto; margin-top: 0; text-decoration: none; display: inline-block; }
a.button-ghost, a.button-outline, a.button-primary,
a.button-danger, a.button-danger-ghost {
  text-decoration: none;
  display: inline-block;
}

/* Tab pill (segmented control) — chrome-spec §3.
 * The track is new: the tabs were previously bare pills on the page
 * background, with no #EDE4CF trough behind them. */
.tabs {
  display: flex;
  gap: 4px;
  width: fit-content;
  padding: 4px;
  background: #EDE4CF;
  border-radius: var(--r-lg);
}
.tab {
  padding: 6px 16px;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 500;
  color: var(--rust);
  background: transparent;
}
.tab:hover { background: var(--cream); color: var(--rust); text-decoration: none; }
.tab:active { background: #D8CDB4; color: var(--rust); }
/* Selected does not respond to hover — it is already the answer. */
.tab-on,
.tab-on:hover,
.tab-on:active { background: var(--forest); color: #F3EEE0; text-decoration: none; }

.card.list { padding: 8px 24px; overflow: hidden; }

.launch-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  /* Padded inward rather than by the card, so the hover background and the
   * click area reach the full width of the row. */
  padding: 16px 24px;
  margin: 0 -24px;
}
.launch-row:last-child { border-bottom: none; }

.launch-main { min-width: 0; }
.launch-title { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.launch-side { display: flex; align-items: center; gap: 16px; flex: none; }

.chip-type {
  padding: 2px 9px;
  border-radius: var(--r-pill);
  background: #EDE1C8;
  color: #7A5B33;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.5px;
}

.row-dates, .row-flags { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
.chip-date {
  padding: 2px 9px;
  border-radius: var(--r-pill);
  background: var(--surface-tint);
  color: var(--text-secondary);
  font-size: 11px;
  font-weight: 500;
}
/* Amber, not red: a date that is not set yet is a prompt, not a failure. */
.chip-flag {
  padding: 2px 9px;
  border-radius: var(--r-pill);
  border: 1px solid #D9C3A2;
  color: #8A6A3F;
  font-size: 11px;
  font-weight: 500;
}

/* Nested controls sit above the row's stretched ::after, so clicking Settings
 * opens Settings and never the launch. .row-link now carries only that
 * stacking and the hit area — the appearance is the shared Ghost variant, and
 * its #F1E9D6 hover is a step darker than the row's #FBF6E9 wash, which is
 * what tells the two apart while the pointer is on the control. */
.row-link {
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  min-height: 32px;
}

/* The whole row opens the launch, not just the name.
 *
 * Done with a stretched pseudo-element rather than by wrapping the row in an
 * anchor: the row also contains a link to Settings, and an anchor inside an
 * anchor is invalid and behaves unpredictably. The ::after covers the row and
 * sits below the Settings link, which is raised above it.
 */
.launch-row { position: relative; }

/* Row states — ui-audit-spec §2.
 *
 * This row used to be deliberately flat: no hover, no colour change, nothing
 * on tap, on the reasoning that it was a surface you touch rather than a
 * button that answers first. That decision has been reversed. With Settings
 * sitting inside the row there are two targets in one strip, and with no
 * hover at all nothing told you the strip itself was one of them.
 *
 * What survives from the old reasoning is the tap-highlight suppression:
 * Safari draws its own pale rounded box around a tapped link, sized to the
 * text rather than the row, which made the title read as a chip. That is
 * still turned off, and the row's own :active state replaces it.
 *
 * The title carries no background, padding or radius — it should look like
 * the text it is. The tap target is the whole row via the stretched ::after,
 * so the link needs no padding of its own.
 */
.launch-row {
  background: #FFFDF6;
  border-bottom: 1px solid #F1E9D6;
  cursor: pointer;
}

/* :has() rather than :focus-within, which would also fire when Settings has
 * focus — the row treatment belongs to the row's own link. */
.launch-row:hover,
.launch-row:has(a.row-title-link:focus-visible) { background: #FBF6E9; }
.launch-row:has(a.row-title-link:active) { background: #F7F0DD; }

/* The name underlines on hover: the row is washed, and this says which of the
 * two things in it you are about to open. */
.launch-row:hover a.row-title-link { text-decoration: underline; }

/* Inset, so a row's focus ring is not clipped by the row below it. */
.launch-row:has(a.row-title-link:focus-visible) {
  outline: 2px solid var(--rust);
  outline-offset: -2px;
}
/* :hover spelled out rather than left to the cascade. There is a generic
 * `a:hover` earlier in this file at the same specificity, so this currently
 * wins on source order alone — which would quietly reverse the day someone
 * moves a block. Naming the state costs one line and removes the question. */
a.row-title-link,
a.row-title-link:active {
  color: var(--text);
  text-decoration: none;
  background: transparent;
  -webkit-tap-highlight-color: transparent;
}
a.row-title-link:hover { color: var(--text); background: transparent; }
.launch-row { -webkit-tap-highlight-color: transparent; }

a.row-title-link::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
}

/* Kept on purpose, and it is not a tap or hover effect: :focus-visible fires
 * for keyboard navigation only, never for a touch or a mouse click. Without
 * it someone tabbing through the portfolio cannot see where they are. Say the
 * word and it goes, but it costs nothing on a phone. */
a.row-title-link:focus-visible::after {
  outline: 2px solid var(--rust);
  outline-offset: -2px;
}

/* Above the stretched area, so it still goes to Settings. */
.row-link { position: relative; z-index: 2; }

/* The old .crumb-sep belonged to the settings back-link pair, which the
 * breadcrumb replaced. It outlived its markup and, sitting later in this file,
 * was quietly overriding the spec values at line 461. */

/* Forms ------------------------------------------------------------------ */

.stack > * + * { margin-top: 18px; }
.stack .field { margin-bottom: 0; }
/* A tight positive gap, not a negative one. The negative margin worked at
 * desktop field heights and overlapped the input on mobile, where the 44px
 * touch targets are taller. */
.stack .hint { margin-top: 6px; }
.stack h2 { margin-top: 26px; }
.stack h2:first-child { margin-top: 0; }

/* Not the `margin` shorthand: `.stack > * + *` sets margin-top for the stack
 * rhythm, and `fieldset.field` outranks it on specificity, so a shorthand here
 * silently collapses the gap above every fieldset. */
fieldset.field {
  border: none;
  padding: 0;
  margin-left: 0;
  margin-right: 0;
  margin-bottom: 0;
}
fieldset.field legend { padding: 0; }

.field-row { display: flex; gap: 16px; }
.field-row .field { flex: 1; min-width: 0; }

/* No margin-top: the 8px below a label is the label's job (ui-audit-spec §4),
 * and a 6px here on top of it made every chips row sit 14px from its label
 * while the pull-outs row sat at 8. */
.chips { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 0; }

/* The radio itself is hidden but still focusable and still the thing the
 * keyboard and screen reader operate — the chip is only its appearance. */
.chip-radio { position: relative; display: inline-flex; }
.chip-radio input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
.chip-radio span {
  padding: 7px 14px;
  border-radius: var(--r-pill);
  border: 1px solid var(--border-strong);
  color: var(--text-secondary);
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
}
.chip-radio input:checked + span {
  background: var(--forest);
  border-color: transparent;
  color: #F3EEE0;
}
.chip-radio input:focus-visible + span { outline: 2px solid var(--rust); outline-offset: 1px; }

.chip-button {
  padding: 7px 14px;
  border-radius: var(--r-pill);
  border: 1px solid var(--border-strong);
  background: transparent;
  color: var(--text-secondary);
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
}
.chip-button.chip-on { background: var(--forest); border-color: transparent; color: #F3EEE0; }

.actions { display: flex; justify-content: flex-end; align-items: center; gap: 10px; }
.actions-left { justify-content: flex-start; }

.input-with-link { display: flex; align-items: center; gap: 10px; }
.input-with-link input { flex: 1; min-width: 0; }
.open-link { font-size: 12px; white-space: nowrap; }

.owner-link { display: flex; gap: 10px; align-items: center; }
.owner-link input {
  flex: 1;
  min-width: 0;
  font-size: 12.5px;
  color: var(--text-secondary);
}

.notice {
  margin: 0;
  padding: 9px 12px;
  font-size: 13px;
  color: #3E4A3C;
  background: #DDE3D6;
  border-radius: var(--r-md);
}

/* Confirmations ---------------------------------------------------------- */

.card.confirm {
  border-color: #DCC69E;
  background: #FBF3E2;
  margin-bottom: 20px;
}
.card.confirm h2 { margin-bottom: 8px; }
.card.confirm p { margin: 0 0 16px; font-size: 13px; color: #6B4A23; }

/* Destructive filled — chrome-spec §3: "same table with #54281A default,
 * #43251C hover/active". Confirm-delete only. */
.button-danger {
  padding: 8px 16px;
  border: 1px solid transparent;
  background: var(--rust);
  color: #F3EEE0;
  font-size: 13px;
}
.button-danger:hover { background: #43251C; color: #F3EEE0; text-decoration: none; }
.button-danger:active { background: #43251C; color: #F3EEE0; }
.button-danger:disabled,
.button-danger[aria-disabled="true"] {
  background: #EDE4CF;
  color: #A79B85;
  cursor: default;
}

/* The cancel half of a confirm pair. chrome-spec has no table for it — it is
 * the Outline variant tinted rust, and is marked here as spec-silent so it is
 * not mistaken for a value someone chose deliberately. */
.button-danger-ghost {
  padding: 7px 14px;
  border: 1px solid #D9C3B8;
  background: transparent;
  color: var(--rust);
  font-size: 13px;
}
.button-danger-ghost:hover { background: #F1E9D6; color: var(--rust); text-decoration: none; }
.button-danger-ghost:active { background: #EDE4CF; color: var(--rust); }
.danger-zone { border-color: #E6D3CB; }

@media (max-width: 900px) {
  .page-head { flex-direction: column; align-items: stretch; }
  .launch-row { flex-direction: column; align-items: stretch; gap: 12px; }
  .launch-side { justify-content: space-between; }
  /* Thumb-sized targets: this is the screen someone opens standing in a
   * hallway deciding which house they are working on. */
  /* No padding: the whole row is the target, so the title does not need to
   * be one, and padding on it was part of what made it read as a chip. */
  a.row-title-link { font-size: 16px; line-height: 1.4; }
  .row-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    min-width: 44px;
    padding: 0 8px;
  }
  .field-row { flex-direction: column; gap: 18px; }
  .card.list { padding: 8px 16px; }
  /* The row bleeds out to the card's edge so the whole width is clickable.
   * The negative margin has to match the card's padding at this breakpoint —
   * the desktop value of 24px overflows a card padded to 16px. */
  .launch-row { padding: 16px; margin: 0 -16px; }
  .card { padding: 20px 16px; }
  .owner-link { flex-direction: column; align-items: stretch; }
  /* Touch targets: this gets used one-handed in a house with bad signal. */
  .chip-radio span, .chip-button, .button-danger, .button-danger-ghost { min-height: 44px; display: inline-flex; align-items: center; }
  input[type="text"], input[type="date"], input[type="url"], input[type="email"], input[type="password"] { min-height: 44px; }
}

/* Launch detail ---------------------------------------------------------- */

.launch-title h1 { margin: 0; }
.page-head .subline + .subline { margin-top: 2px; }

.progress-panel {
  display: flex;
  gap: 32px;
  align-items: flex-start;
}
.progress-figure { flex: none; min-width: 120px; }
.stat { display: block; font-size: 40px; font-weight: 600; line-height: 1; }
.stat-sub { display: block; margin-top: 6px; font-size: 12px; color: var(--text-muted); }
.progress-bars { flex: 1; min-width: 0; }

/* Segmented per phase, widths proportional to each phase's weight — so the
 * bar shows where the work actually is, not five equal fifths. */
.segmented { display: flex; gap: 2px; margin-bottom: 20px; }
.seg { height: 8px; background: #EBE1C9; border-radius: 4px; overflow: hidden; min-width: 8px; }
.seg-fill { height: 100%; background: var(--rust); border-radius: 4px; }
.seg-fill.seg-na {
  width: 100% !important;
  background: repeating-linear-gradient(45deg, #DED3B8 0 4px, #EBE1C9 4px 8px);
}

.phase-rail { display: grid; gap: 10px; }
.phase { display: grid; grid-template-columns: 14px 1fr auto; gap: 10px; align-items: center; }
.phase-dot {
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--surface); border: 2px solid #CFC3A8;
}
.phase-done .phase-dot { background: var(--forest); border-color: var(--forest); }
.phase-active .phase-dot { border: 3px solid var(--rust); }
.phase-na .phase-dot { border-style: dashed; }
.phase-name { font-size: 13px; font-weight: 500; }
.phase-todo .phase-name, .phase-na .phase-name { color: var(--text-muted); }
.phase-sub { font-size: 12px; color: var(--text-muted); }

/* A phase and its steps are one block; the page column's gap separates the
 * blocks. Inside, the steps keep the 8px rhythm of a checklist rather than the
 * 20px of stacked cards — sixteen steps 20px apart is a different screen. */
.phase-group { display: flex; flex-direction: column; gap: 8px; }
.phase-group .card.step { margin-bottom: 0; }

.phase-heading {
  font-family: Rubik, sans-serif;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 2px;
  color: var(--text-faint);
  margin: 0 0 2px;
}

/* Step cards ------------------------------------------------------------- */

.card.step { padding: 0; margin-bottom: 8px; border-radius: var(--r-lg); }
.step-na { opacity: 0.62; }

/* `color: inherit` matters as much as `font-family: inherit` here. A <button>
 * does not inherit colour — the UA gives it `buttontext`, which is pure black —
 * so .step-name, which sets no colour of its own, rendered #000000 rather than
 * the palette's #241B12. Not browser-default blue, as was reported; a colour
 * that is not in the palette at all, and only visible beside text that is. */
.step-head {
  color: inherit;
  display: grid;
  grid-template-columns: 28px 1fr auto 16px;
  gap: 12px;
  align-items: center;
  width: 100%;
  padding: 14px 18px;
  background: transparent;
  border: none;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}
.step-num {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-faint);
  text-align: center;
}
.step-headline { min-width: 0; }
.step-name { display: block; font-size: 14px; font-weight: 500; }
.step-sub { display: block; margin-top: 2px; font-size: 12px; color: var(--text-muted); }
.step-pillbox { display: flex; align-items: center; gap: 8px; }
.chev { font-size: 11px; color: var(--text-faint); transition: transform .15s; }
.step-open .chev { transform: rotate(180deg); }

/* Hidden everywhere, not only where it collided.
 *
 * Measured at 393pt with a mark injected — no dev step is manual — the mark is
 * 87px wide inside an 88px pillbox and overlaps the status pill. But the reason
 * it goes is not the collision: the expanded step already carries the same
 * information as a full sentence, "Set by hand — ticking no longer moves it",
 * with the control to undo it. Two statements of one fact, and the collapsed
 * one is the one that cannot explain itself. */
.manual-mark { display: none; }


.pill {
  padding: 4px 12px;
  border-radius: var(--r-pill);
  font-size: 11px;
  font-weight: 600;
  border: 1px solid transparent;
  white-space: nowrap;
}
.pill-complete { background: var(--forest); color: #F3EEE0; }
.pill-in_progress { background: #EDDFC3; color: #6B4A23; border-color: #DCC69E; }
.pill-not_started { color: var(--text-muted); border-color: #D8CDB6; }
.pill-na { color: var(--text-faint); border-color: #D8CDB6; border-style: dashed; }

.step-body { padding: 4px 18px 18px; border-top: 1px solid var(--border-soft); }
.step-body > * + * { margin-top: 16px; }
.step-note {
  margin: 14px 0 0;
  font-style: italic;
  font-size: 12.5px;
  color: var(--text-muted);
}
.lock-note {
  margin: 0;
  padding: 8px 11px;
  font-size: 12.5px;
  color: #6B4A23;
  background: #FBF3E2;
  border-radius: var(--r-md);
}

.status-row .chips, .flow .chips { margin-top: 6px; margin-bottom: 6px; }
.inline-field { max-width: 280px; }
.chip-danger-on { background: var(--rust); border-color: transparent; color: #F3EEE0; }

.linkish {
  background: none;
  border: none;
  padding: 0;
  font-family: inherit;
  font-size: inherit;
  color: var(--rust);
  text-decoration: underline;
  cursor: pointer;
}

/* Checklist -------------------------------------------------------------- */

.sub-heading {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.5px;
  color: var(--text-faint);
  margin: 16px 0 6px;
}
.checklist > .sub-heading:first-child { margin-top: 0; }

.sub { display: flex; align-items: flex-start; gap: 10px; padding: 3px 0; }
.sub-indent { margin-left: 28px; }

.tickbox {
  flex: none;
  width: 18px; height: 18px;
  margin-top: 1px;
  border-radius: var(--r-sm);
  border: 1px solid #C9BCA0;
  background: var(--surface);
  color: #F3EEE0;
  font-size: 11px;
  line-height: 15px;
  text-align: center;
  padding: 0;
  cursor: pointer;
}
.tickbox.ticked { background: var(--forest); border-color: var(--forest); }
.tickbox:disabled { border-style: dashed; cursor: not-allowed; opacity: 0.6; }

.sub-text { flex: 1; font-size: 13px; min-width: 0; }
.ticked ~ .sub-text { color: #9A8F79; text-decoration: line-through; text-decoration-color: #C9BCA0; }
.sub-text.editing {
  outline: 2px solid var(--rust);
  outline-offset: 2px;
  border-radius: 3px;
  text-decoration: none;
  color: var(--text);
}

/* Row actions stay out of the way until the row is touched or focused. */
.sub-actions { display: flex; gap: 2px; opacity: 0; transition: opacity .12s; }
.sub:hover .sub-actions, .sub:focus-within .sub-actions { opacity: 1; }
.icon-button {
  border: none;
  background: none;
  padding: 2px 6px;
  font-size: 13px;
  line-height: 1;
  color: var(--text-faint);
  cursor: pointer;
}
/* chrome-spec §3, "Rule to delete": no button's hover may change only its text
 * colour. There is no literal `button:hover { color: … }` in this file — this
 * was it, on the comment and substep row actions. It keeps the colour change
 * and gains the ghost hover background, so the state reads as a target rather
 * than as text that happened to darken. */
.icon-button:hover { background: #F1E9D6; color: var(--rust); border-radius: var(--r-sm); }

.checklist-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--border-soft);
}
.checklist-actions .spacer { flex: 1; }
.button-ghost.small, .button-outline.small,
.button-primary.small { padding: 5px 11px; font-size: 12px; }

.add-row { display: flex; gap: 8px; margin-top: 10px; }
.add-row input { flex: 1; min-width: 0; }

textarea {
  width: 100%;
  padding: 9px 11px;
  font-family: inherit;
  font-size: 13px;
  color: var(--text);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  resize: vertical;
}

/* Save indicator --------------------------------------------------------- */
/* Bottom-left so it never sits under a thumb reaching for the checklist. */

.save-status {
  position: fixed;
  left: 16px;
  bottom: 16px;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 14px;
  border-radius: var(--r-pill);
  font-size: 12.5px;
  font-weight: 500;
  box-shadow: 0 4px 16px rgba(36, 27, 18, 0.12);
}
/* Scroll room under the page while the banner is up, so no row is stuck
 * behind it. The 80px is the page's own bottom padding. */
body.has-save-banner .page {
  padding-bottom: calc(80px + var(--save-banner-space, 0px));
}

.save-ok { background: #DDE3D6; color: #3E4A3C; }
.save-busy { background: var(--surface-tint); color: var(--text-secondary); }
.save-error { background: #EAD8D1; color: var(--rust); }
.save-error .linkish { color: var(--rust); }

@media (max-width: 900px) {
  /* `align-items: flex-start` means "top-align" in the desktop row and
   * "do not stretch" once the direction flips to column — the cross axis
   * changes with it. The bars sized to their content at 220px inside a 329px
   * card, leaving 108px of the right-hand third unused on every phase row. */
  .progress-panel { flex-direction: column; gap: 20px; align-items: stretch; }
  .progress-figure { display: flex; align-items: baseline; gap: 12px; }
  .stat-sub { margin-top: 0; }
  .step-head { grid-template-columns: 22px 1fr auto; padding: 12px 14px; gap: 8px; }
  .step-head .chev { display: none; }
  .step-pillbox { flex-direction: column; align-items: flex-end; gap: 4px; }
  .step-body { padding: 4px 14px 16px; }
  .inline-field { max-width: none; }

  /* 44px rows and 24px boxes: this is the screen someone actually ticks
   * against, standing in a room holding a phone. */
  .sub { padding: 6px 0; min-height: 44px; align-items: center; }
  .tickbox { width: 24px; height: 24px; border-radius: 7px; font-size: 13px; line-height: 21px; }
  .sub-text { font-size: 14px; }
  .sub-actions { opacity: 1; }
  .icon-button { padding: 10px 8px; }
  .save-status { left: 12px; right: 12px; bottom: 12px; justify-content: center; }
}

/* Punch list ------------------------------------------------------------- */

.stat-row { display: flex; gap: 28px; flex-wrap: wrap; padding: 18px 24px; }
.stat-cell { display: flex; flex-direction: column; gap: 2px; }
.stat-n { font-size: 22px; font-weight: 600; line-height: 1.2; }
.stat-money { font-size: 20px; }
.stat-l { font-size: 10px; font-weight: 600; letter-spacing: 1.5px; color: var(--text-faint); }
.stat-wide { margin-left: auto; text-align: right; }
.stat-wide .stat-cell, .stat-wide { align-items: flex-end; }
/* The caveats sit with the number, not in a footnote — an unquoted item is
 * missing money, and a total that hides that is a total that understates. */
.stat-caveat { margin-top: 3px; font-size: 11.5px; color: var(--text-muted); }

.filters { padding: 14px 18px; }
/* Search, then the two ways to get items in.
 *
 * "＋ Add item" used to be the only filled-primary add control in the app —
 * every other one is Outline — which made adding items one at a time the
 * loudest thing on a screen whose own empty state says they "come out of the
 * preliminary walkthrough, around fifty for a typical house". Both are Outline
 * now, and the pair is separated from the search field by a wider gap so it
 * reads as one set of two options rather than three equally spaced controls.
 * margin-left:auto does nothing here — the search input is flex:1 and has
 * already eaten the free space — so the separation is an explicit margin.
 *
 * Not done, and worth revisiting if the punch list gets a proper empty state:
 * making Import the primary while the list is empty. Conditional emphasis is
 * more machinery than this earns. */
.filters-top { display: flex; gap: 8px; align-items: center; }
.filters-top input[type="search"] { flex: 1; min-width: 0; }
.filters-top [data-add-item] { margin-left: 8px; }   /* 8 + the 8px gap = 16 */
.filters-body { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 10px; }
/* Desktop shows the dropdowns whatever the details element thinks. */
.filters-summary { display: none; }
.filters-wrap { margin: 0; }

/* Filter menus ------------------------------------------------------------
   A <details> per column holding checkboxes. Multi-select without a script:
   the boxes are part of the GET form, so repeated names arrive as repeated
   query parameters and the server does the rest. */
.filter-menu { position: relative; }
.filter-menu > summary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  font-size: 12.5px;
  font-weight: 500;
  color: var(--rust);
  cursor: pointer;
  white-space: nowrap;
  list-style: none;
}
.filter-menu > summary::-webkit-details-marker { display: none; }
.filter-menu > summary::after { content: '▾'; font-size: 9px; color: var(--text-muted); }
.filter-menu > summary:hover { background: var(--surface-tint); }
.filter-menu[open] > summary { background: var(--cream); }
.filter-menu.is-on > summary { border-color: var(--clay); color: var(--rust); }

/* The panel floats over the list rather than pushing it down, so ticking two
   rooms does not move the table out from under the pointer. */
.filter-opts {
  position: absolute;
  z-index: 30;
  top: calc(100% + 4px);
  left: 0;
  min-width: 200px;
  max-width: 320px;
  max-height: 320px;
  overflow-y: auto;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  box-shadow: 0 8px 24px rgba(36, 27, 18, 0.12);
}
.filter-opt {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border-radius: var(--r-sm);
  font-size: 13px;
  color: var(--text);
  cursor: pointer;
}
.filter-opt:hover { background: var(--surface-tint); }
/* Room names are shown whole here even where the table truncates them: a
   filter you cannot read is not a filter. */
.filter-opt span { white-space: normal; }
.filter-opt input { flex: none; margin: 0; }

/* Active-filter chips ---------------------------------------------------- */
.filter-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.chip-filter {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px 4px 10px;
  background: var(--surface-tint);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  font-size: 12px;
  color: var(--rust);
  text-decoration: none;
}
.chip-filter:hover { background: var(--cream); text-decoration: none; }
.chip-x { font-size: 13px; line-height: 1; color: var(--text-muted); }
.chip-filter:hover .chip-x { color: var(--rust); }

.list-count { color: var(--text-muted); }

/* A row edited out of the filter it was found under. It keeps its place until
   the next load rather than vanishing under the pointer mid-edit, so it needs
   to say why it is still here. */
.punch-row.row-unmatched { background: var(--surface-tint); }
.unmatched-note {
  display: inline-block;
  margin-top: 2px;
  font-size: 11px;
  font-style: italic;
  color: var(--text-muted);
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
/* A <select> sizes itself to its widest option and, unlike the inputs above,
 * had no width of its own. In the drawer's ROOM / SCHEDULED pair that meant a
 * long room name — "GUEST BEDROOM 1 (Cassette Tape Room)" is a real one — grew
 * the select to 292px inside a 185px field, overflowing it by 107px and
 * running 91px underneath the date input, covering its chevron. Measured, not
 * guessed: with "Kitchen" selected there is no overlap at all, which is why it
 * only shows up on some items.
 *
 * width:100% makes it obey its container; min-width:0 lets that container
 * shrink inside a flex row; ellipsis truncates the label instead of spilling.
 * The full name is still in the open list and in the title attribute. */
select {
  min-width: 0;
  padding: 8px 10px;
  font-family: inherit;
  font-size: 13px;
  color: var(--text);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  text-overflow: ellipsis;
}

/* Only a select that is a form field fills its field. The filters row, the
 * import review's meta row and the wizard's pull-out row all sit selects
 * side by side in flex rows, where a blanket width:100% stretched each one to
 * the full row — 1086px on the punch list filters. */
.field select { width: 100%; }
input[type="search"] {
  padding: 8px 11px;
  font-family: inherit;
  font-size: 13px;
  color: var(--text);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
}

.bulk-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 12px;
  padding: 10px 18px;
  background: var(--surface-tint);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  font-size: 13px;
  font-weight: 500;
}
/* One row on a wide screen — the count, the actions, then Clear. The two
 * groups exist for the phone's two lines and step out of the way here. */
.bulk-head, .bulk-actions { display: contents; }
.bulk-bar [data-bulk-clear] { order: 1; }

/* The Change sheet, as a dropdown under its button on a wide screen. Hung from
 * the button's left edge and opening rightwards: the bar starts at its count,
 * so Change sits near the left of it with the rest of the bar's width to its
 * right. Hung from the right edge, as first written, it opened 15px from the
 * screen's edge at 1280 and would have run off it on anything narrower. Up to
 * 900px it is a sheet at the foot of the screen instead (below). */
.bulk-change { position: relative; }
.bulk-scrim { display: none; }
.bulk-sheet {
  position: absolute;
  left: 0;
  top: calc(100% + 6px);
  z-index: 20;
  width: 320px;
  padding: 10px 14px 14px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  box-shadow: 0 8px 24px rgba(36, 27, 18, 0.10);
  font-weight: 400;
}
.bulk-sheet:focus { outline: none; }
.bulk-sheet-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.bulk-sheet-title { margin: 0; font-size: 13px; font-weight: 600; color: var(--text); }
.bulk-sheet .field { margin: 10px 0 0; }

.punch-table { padding: 0; overflow: hidden; }

/* The table's own toolbar: a full-width row inside the card, above the head.
 *
 * The legend used to sit on the page background between the filters card and
 * the table, which made a key to the dot column read as another filter. It is
 * inside the card it explains now, and the row it sits in carries the two
 * other things that describe the list rather than filter it — how many rows
 * are showing, and the way back to the default sort. */
.punch-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 10px 18px;
  border-bottom: 1px solid var(--border-soft);
}
.punch-toolbar-aside {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-left: auto;
  font-size: 11.5px;
  color: var(--text-muted);
}

.nec-legend {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: 11.5px;
  color: var(--text-muted);
}
.nec-legend .nec-dot { margin-left: 10px; }
.nec-legend .nec-dot:first-child { margin-left: 0; }

.punch-head, .punch-row {
  display: grid;
  grid-template-columns: 34px 22px minmax(0, 1fr) 116px 104px 96px 118px 108px;
  gap: 10px;
  align-items: center;
  padding: 10px 18px;
}
.punch-head {
  background: var(--surface-raised);
  border-bottom: 1px solid var(--border);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 1.5px;
  color: var(--text-faint);
}
.punch-row {
  position: relative;
  border-bottom: 1px solid #F1E9D6;
  -webkit-tap-highlight-color: transparent;
}
/* Above the stretched area, so selecting a row does not open it. */
.punch-row .col-check { position: relative; z-index: 2; }
.punch-row:last-child { border-bottom: none; }
.punch-row.row-selected { background: #F7F0DD; }
.punch-row.row-checked { background: #F3EBD6; }
.punch-row.status-done { background: #E7ECDF; }
/* Declined is settled and not coming back — greyed and struck, so it reads as
 * closed rather than as an item nobody has got to. */
.punch-row.status-declined { opacity: 0.45; }
.punch-row.status-declined .punch-title { text-decoration: line-through; text-decoration-color: #B3A78E; }

/* The whole row opens the item, the same way the portfolio works.
 *
 * Stretched pseudo-element rather than wrapping the row in an anchor: the row
 * also holds a selection checkbox, and an anchor around a button swallows the
 * button's clicks. The ::after covers the row; the checkbox is raised above.
 *
 * Deliberately flat — no hover, no tap highlight. Safari otherwise draws a
 * pale box around the title text, which makes it read as a chip rather than a
 * row you can touch.
 */
a.col-title {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  padding: 0;
  border: none;
  background: none;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  color: inherit;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
a.col-title:hover, a.col-title:active { color: inherit; text-decoration: none; }
a.col-title::after { content: ''; position: absolute; inset: 0; z-index: 1; }
a.col-title:focus-visible::after { outline: 2px solid var(--rust); outline-offset: -2px; }
.punch-title {
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.punch-meta { font-size: 11px; color: var(--text-muted); }
.col-room, .col-type, .col-owner {
  font-size: 12.5px;
  color: var(--text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.col-cost { font-size: 12.5px; text-align: right; white-space: nowrap; }
.col-nec { text-align: center; }
.nec-dot { font-size: 13px; line-height: 1; }
.nec-necessary { color: var(--rust); }
.nec-recommended { color: var(--text-faint); }

/* The eight statuses, from tokens.md. Neutral warms through the clay family
 * as work is committed, then forest when done; rust only for declined. */
.punch-pill-identified { color: #8A7E68; border-color: #D8CDB6; }
.punch-pill-quoted { background: #F1E9D4; color: #7A6E58; }
.punch-pill-sent_to_owner { color: #8A6A3F; border-color: #D9C3A2; }
.punch-pill-approved { background: #DDE3D6; color: #3E4A3C; }
.punch-pill-declined { background: #EAD8D1; color: var(--rust); }
.punch-pill-scheduled { color: #5C6B58; border-color: #B8C2AE; }
.punch-pill-in_progress { background: #E5CFA6; color: #5F421D; }
.punch-pill-done { background: var(--forest); color: #F3EEE0; }

/* Drawer ----------------------------------------------------------------- */
/* The only shadowed element in the design. */

.drawer-scrim {
  position: fixed;
  inset: 0;
  background: rgba(36, 27, 18, 0.18);
  z-index: 19;
}
.drawer {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: 440px;
  max-width: 95vw;
  z-index: 20;
  padding: 20px 26px 40px;
  background: var(--surface);
  border-left: 1px solid var(--border);
  box-shadow: -16px 0 40px rgba(36, 27, 18, 0.12);
  overflow-y: auto;
  transition: width .18s ease;
}
.drawer.drawer-wide { width: 760px; }
.drawer > * + * { margin-top: 16px; }
.drawer .field { margin-bottom: 0; }
.drawer-head { display: flex; align-items: center; gap: 10px; }
.drawer-head .spacer { flex: 1; }
/* The label only earns its place on mobile, where the drawer covers the whole
 * screen and there is otherwise nothing saying what you are looking at. */
.drawer-heading { display: none; }
.drawer-close {
  border: none;
  background: none;
  padding: 4px 8px;
  font-size: 20px;
  line-height: 1;
  color: var(--text-muted);
  cursor: pointer;
}
.drawer-close:hover { color: var(--rust); }
.drawer-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 24px;
  padding-top: 16px;
  border-top: 1px solid var(--border-soft);
}

@media (max-width: 900px) {
  .stat-row { gap: 16px 24px; padding: 16px; }
  .stat-wide { margin-left: 0; text-align: left; align-items: flex-start; width: 100%; }
  /* The search box takes its own row; the pair shares the next one, half each,
   * so it still reads as a pair rather than two full-width bars. */
  .filters-top { flex-wrap: wrap; align-items: stretch; }
  .filters-top input[type="search"] { flex: 1 0 100%; }
  .filters-top [data-add-item],
  .filters-top a.button-outline { flex: 1 1 0; width: auto; margin-left: 0; }
  .filters-body { flex-direction: column; align-items: stretch; margin-top: 12px; }
  /* Scoped to the controls that should stretch. The blanket ".filters input,
     .filters a" this replaces also caught the filter checkboxes and the
     active-filter chips, which became full-width 44px blocks. */
  .filters-top input[type="search"],
  .filters-top > button, .filters-top > a,
  .filters-body > button, .filters-body > a { width: 100%; min-height: 44px; }
  .filters-body > .filter-menu { width: 100%; }
  .filters-body > .filter-menu > summary { width: 100%; min-height: 44px; justify-content: space-between; }
  /* Floating over a stacked column would cover the menus below it. */
  .filter-opts { position: static; max-width: none; box-shadow: none; margin-top: 6px; }
  .filters-summary {
    /* flex, not block: with a min-height, block leaves the label at the top of
       the box. Row direction, so align-items is the vertical one. */
    display: flex;
    align-items: center;
    min-height: 44px;
    margin-top: 12px;
    padding: 10px 0;
    font-size: 13px;
    font-weight: 500;
    color: var(--rust);
    cursor: pointer;
  }
  .filters-wrap:not([open]) .filters-body { display: none; }

  /* The table becomes cards: eight columns do not survive 375px, and the
   * phone is where this list is actually worked. */
  .punch-head { display: none; }
  .punch-table { padding: 8px; background: transparent; border: none; }
  /* The card loses its surface here and the rows become cards of their own, so
   * a bordered toolbar strip would be a line floating over the page. Keep the
   * legend — the dots are still the only place necessity appears — but let it
   * sit on the background with the rest of the page. */
  .punch-toolbar {
    padding: 4px 6px 10px;
    border-bottom: none;
    row-gap: 6px;
  }
  .punch-toolbar-aside { margin-left: 0; }
  .punch-row {
    display: grid;
    grid-template-columns: 44px minmax(0, 1fr);
    grid-template-areas:
      "check title"
      "check meta"
      "check pills";
    gap: 4px 10px;
    padding: 14px;
    margin-bottom: 8px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
  }
  /* The button fills the whole 44px target rather than sitting inside a
   * larger padded cell. A padded cell left dead corners: above the row's
   * stretched link so they blocked opening, outside the button so they did
   * not select either — a region that did nothing at all.
   *
   * The visible mark is drawn inset with a border, so a 44px target does not
   * mean a heavy 44px square in the corner of every card. */
  .punch-row .col-check {
    grid-area: check;
    align-self: start;
    position: relative;
    z-index: 2;
    margin: -10px 0 0 -10px;
  }
  .punch-row .col-check .tickbox {
    width: 44px;
    height: 44px;
    border: none;
    background: transparent;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0;
  }
  .punch-row .col-check .tickbox::before {
    content: '';
    width: 26px;
    height: 26px;
    border: 1.5px solid #C9BCA0;
    border-radius: 7px;
    background: var(--surface);
  }
  .punch-row .col-check .tickbox.ticked::before {
    background: var(--forest);
    border-color: var(--forest);
  }
  /* The glyph, not two rotated gradients. The gradient version rendered as a
   * chevron rather than a tick, which is the sort of thing that looks fine in
   * the CSS and wrong on the screen. */
  .punch-row .col-check .tickbox.ticked::after {
    content: '✓';
    position: absolute;
    color: #F3EEE0;
    font-size: 15px;
    line-height: 1;
  }
  .punch-row .col-check .tickbox { position: relative; }
  .punch-row .col-nec { display: none; }
  /* The title's tap target. Measured 239x21 on a one-line title and 239x40 on
     two — its ::after is inset 0 like the portfolio card's, but .col-title is
     its own containing block, so the pseudo covers the title cell and not the
     row. No justify-content: the extra height must go below the text, because
     .col-title is baseline-aligned to the necessity dot and centring the
     content would carry the baseline down with it. */
  .punch-row .col-title { grid-area: title; min-height: 44px; }
  .punch-title { white-space: normal; font-size: 14px; }
  .punch-row .col-room { grid-area: meta; font-size: 12px; }
  .punch-row .col-type, .punch-row .col-owner { display: none; }
  .punch-row .col-status { grid-area: pills; justify-self: start; margin-top: 4px; }
  .punch-row .col-cost { grid-area: pills; justify-self: end; text-align: right; margin-top: 4px; }

  /* Pinned below both bars rather than under them. top: 8px parked it beneath
     the header and the crumb bar, which sit above it at z-index 20 and 19:
     pinned at 393pt, its first row of buttons was covered and a tap on "Mark
     done" landed on the header. The offset is the two bars' own variables plus
     their 1px borders, so the three cannot drift apart. */
  .bulk-bar {
    position: sticky;
    top: calc(var(--header-height) + 1px + var(--crumb-height) + 1px + 8px);
    z-index: 5;
    flex-wrap: wrap;
    row-gap: 8px;
  }
  /* Raised while its sheet is open. The bar is a stacking context, so the sheet
     and scrim inside it rise no higher than the bar does — 5, under the header. */
  .bulk-bar.bulk-sheet-open { z-index: 30; }
  /* Two lines: what is selected and the way out, then what to do with it. */
  .bulk-head { display: flex; flex-basis: 100%; align-items: center; justify-content: space-between; }
  .bulk-actions { display: flex; align-items: center; gap: 10px; }
  .bulk-bar [data-bulk-clear] { order: 0; }

  /* A sheet at the foot of the screen: where a thumb is, and where it cannot
     open below the fold the way a dropdown from an unpinned bar would. */
  .bulk-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 1;
    background: rgba(36, 27, 18, 0.18);
  }
  .bulk-sheet {
    position: fixed;
    top: auto;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 2;
    width: auto;
    padding: 12px 16px calc(16px + env(safe-area-inset-bottom, 0px));
    border-width: 1px 0 0;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
  }
  /* 44px, like every other control here. The icon button's own padding made
     the way out of the sheet 24x33 at 393pt. */
  .bulk-sheet-head .icon-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
  }
  /* Clear was 31x16 among four 44px buttons. Pre-existing, and left that way it
     is the one control in the bar a thumb misses. Scoped to the bar rather than
     changed on .linkish, which the save-error banner also uses — that one sits
     in a sentence and takes the inline-link exemption. */
  .bulk-bar [data-bulk-clear] {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 4px;
  }

  /* Full screen on a phone, so it has to clear the notch and the status bar —
   * top: 0 alone put the close button under them. The header then sticks, so
   * the way out stays reachable however far down the form you are. */
  .drawer {
    width: 100%;
    max-width: none;
    padding: 0 16px calc(40px + env(safe-area-inset-bottom, 0px));
  }
  .drawer.drawer-wide { width: 100%; }
  .drawer-head [data-drawer-wider] { display: none; }
  /* The item's name, with the category as an eyebrow above it. min-width: 0
   * so the name can truncate rather than push the close button off. */
  .drawer-heading { display: block; min-width: 0; }
  .drawer-eyebrow {
    display: block;
    font-size: 9.5px;
    font-weight: 600;
    letter-spacing: 1.5px;
    color: var(--text-faint);
  }
  .drawer-item-name {
    display: block;
    font-size: 14px;
    font-weight: 500;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .drawer-head {
    position: sticky;
    top: 0;
    z-index: 1;
    margin: 0 -16px;
    padding: calc(10px + env(safe-area-inset-top, 0px)) 16px 10px;
    background: var(--surface);
    border-bottom: 1px solid var(--border-soft);
  }
  .drawer > * + * { margin-top: 16px; }
  .drawer-close { padding: 10px 12px; font-size: 24px; min-height: 44px; min-width: 44px; }
}

/* Photos and comments ---------------------------------------------------- */

.drawer-section { border-top: 1px solid var(--border-soft); padding-top: 16px; }

.photo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
  gap: 8px;
  margin-top: 8px;
}
.photo-grid:empty { display: none; }

.photo { position: relative; margin: 0; }
.photo img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  display: block;
  background: var(--surface-tint);
}
.photo-remove {
  position: absolute;
  top: 4px; right: 4px;
  width: 22px; height: 22px;
  border: none;
  border-radius: 50%;
  background: rgba(36, 27, 18, 0.62);
  color: #F3EEE0;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: opacity .12s;
}
.photo:hover .photo-remove, .photo:focus-within .photo-remove { opacity: 1; }

.photo-add { display: inline-block; margin-top: 10px; cursor: pointer; }
.photo-add .button-ghost, .photo-add .button-outline { display: inline-block; }
.hint-error { color: var(--rust); font-weight: 500; }
.hint-warn { color: #8A6A3F; }

.comments { margin-top: 8px; }
.comment {
  padding: 10px 0;
  border-bottom: 1px solid var(--border-soft);
}
.comment:last-child { border-bottom: none; }
.comment-head { display: flex; align-items: baseline; gap: 8px; }
.comment-author { font-size: 13px; font-weight: 500; }
.comment-when { font-size: 11.5px; color: var(--text-muted); flex: 1; }
.comment-body {
  margin: 4px 0 0;
  font-size: 13px;
  line-height: 1.5;
  white-space: pre-wrap;
}
.comment .icon-button { opacity: 0; }
.comment:hover .icon-button, .comment:focus-within .icon-button { opacity: 1; }

.comment-compose { position: relative; margin-top: 12px; }
.comment-compose .actions { margin-top: 8px; justify-content: space-between; }

/* Anchored to the composer rather than the caret: a caret-following menu on a
 * wrapping textarea is a lot of arithmetic for very little. */
.mention-menu {
  position: absolute;
  left: 0;
  bottom: 100%;
  margin-bottom: 4px;
  z-index: 5;
  min-width: 180px;
  padding: 4px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: 0 6px 20px rgba(36, 27, 18, 0.12);
}
.mention-option {
  display: block;
  width: 100%;
  padding: 7px 10px;
  border: none;
  border-radius: var(--r-sm);
  background: transparent;
  font-family: inherit;
  font-size: 13px;
  text-align: left;
  color: var(--text);
  cursor: pointer;
}
.mention-active { background: var(--forest); color: #F3EEE0; }

@media (max-width: 900px) {
  .photo-grid { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); }
  /* Always visible: there is no hover on a phone. */
  .photo-remove { opacity: 1; width: 28px; height: 28px; font-size: 16px; }
  .comment .icon-button { opacity: 1; }
  .photo-add .button-ghost, .photo-add .button-outline { width: 100%; text-align: center; min-height: 44px; }
  .mention-option { min-height: 44px; }
}

/* Photo staging ---------------------------------------------------------- */
/* Photos wait here until they are sent, so several camera trips become one
 * upload. Visually distinct from the uploaded grid — a pending photo is not
 * a saved photo, and the difference should be obvious at a glance. */

.photo-staging {
  margin-top: 10px;
  padding: 10px;
  background: var(--surface-tint);
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-md);
}
.photo-staging .photo-grid { margin-top: 0; }

/* A photo mid-conversion. The image underneath is the HEIC the browser cannot
   draw, so the square is empty until the JPEG replaces it — the label is what
   distinguishes "working on it" from the broken thumbnail this replaced. */
.photo-converting { position: relative; background: var(--surface-tint); }
.photo-converting img { opacity: 0; }
.photo-note {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  color: var(--text-muted);
}

.photo-pending img {
  border-style: dashed;
  opacity: 0.85;
}

.staging-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-top: 10px;
}

@media (max-width: 900px) {
  .staging-actions { flex-direction: column; align-items: stretch; }
  .staging-actions button { width: 100%; min-height: 44px; }
  /* Always visible on touch — a pending photo you cannot remove is worse
   * than one you have to upload. */
  .photo-pending .photo-remove { opacity: 1; }
}

/* A thumbnail that failed to load, which is not the same thing as a photo
 * that failed to upload — the bytes are in the bucket either way. */
.photo-broken img { opacity: 0.35; }
.photo-retry {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 3;
  padding: 4px 10px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  background: var(--surface);
  font-family: inherit;
  font-size: 11px;
  font-weight: 500;
  color: var(--rust);
  cursor: pointer;
}

/* ---------------------------------------------------------------------------
   Supply wizard
   --------------------------------------------------------------------------- */

/* The rail is a progress indicator and a jump target both: someone correcting
   one answer should not have to walk the other seven. Horizontal and
   scrollable rather than the vertical .phase-rail, because eight short labels
   read better across than down and the phone has one thumb. */
.wizard-rail {
  display: flex;
  gap: 6px;
  /* Explicitly zero, not merely absent: this is an <ol>, and dropping the old
   * margin-bottom let the browser's own 1em block margin back in. */
  margin: 0;
  padding: 0 0 6px;
  list-style: none;
  overflow-x: auto;
  scrollbar-width: none;
}
.wizard-rail::-webkit-scrollbar { display: none; }
.wizard-rail li { flex: 0 0 auto; }

.rail-step {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 7px 12px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  cursor: pointer;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
}
.rail-number {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  font-size: 11px;
  border-radius: var(--r-pill);
  background: var(--surface-tint);
  color: var(--text-secondary);
}
.rail-step.is-done { color: var(--text-secondary); border-color: var(--border-strong); }
.rail-step.is-done .rail-number { background: var(--sage); color: #FFFDF6; }
.rail-step.is-current {
  color: #F3EEE0;
  background: var(--forest);
  border-color: var(--forest);
}
.rail-step.is-current .rail-number { background: rgba(255, 253, 246, 0.22); color: #F3EEE0; }

.wizard-panel h2 { margin-top: 0; }

/* Tabs only appear on a compound. A single unit gets no chrome for a choice
   it does not have. */
.unit-tabs {
  display: flex;
  gap: 6px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}
.unit-tab {
  padding: 6px 14px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-secondary);
  background: transparent;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.unit-tab.is-active { color: #F3EEE0; background: var(--rust); border-color: var(--rust); }

.grid-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 14px;
}
.field.compact { margin: 0; }

.pullouts { display: grid; gap: 10px; margin-bottom: 12px; }
.pullout-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.pullout-row select,
.pullout-row input[type="text"] {
  padding: 8px 10px;
  font-family: inherit;
  font-size: 14px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
}
.pullout-row input[type="text"] { flex: 1 1 160px; }

.check { display: flex; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; }

/* Multi-select chips. The single-select .chip-radio already exists; these
   differ only in that several can be on at once. */
.chip-check {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.chip-check input { position: absolute; opacity: 0; pointer-events: none; }
.chip-check span {
  display: inline-block;
  padding: 7px 14px;
  font-size: 13px;
  color: var(--text-secondary);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
}
.chip-check input:checked + span {
  color: #F3EEE0;
  background: var(--forest);
  border-color: var(--forest);
}
.chip-check input:focus-visible + span { outline: 2px solid var(--rust); outline-offset: 2px; }

.review { display: grid; gap: 10px; margin: 0 0 20px; }
.review > div {
  display: grid;
  grid-template-columns: 130px 1fr;
  gap: 12px;
  padding-bottom: 9px;
  border-bottom: 1px solid var(--border-soft);
}
.review dt { font-size: 12px; color: var(--text-muted); }
.review dd { margin: 0; font-size: 14px; }

.warnings {
  padding: 14px 16px;
  margin-bottom: 20px;
  background: #FBF3E2;
  border: 1px solid #DCC69E;
  border-radius: var(--r-lg);
}
.warnings h3 { margin: 0 0 8px; }
.warnings ul { margin: 0; padding-left: 18px; }
.warnings li { font-size: 13px; color: var(--text-secondary); margin-bottom: 4px; }

.confirm-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; }

.wizard-generate { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

/* The wizard's panel and its nav live inside the form, out of reach of the
 * page column's gap, so the form carries the same 20px itself. */
.wizard { display: flex; flex-direction: column; gap: 20px; }

.wizard-nav {
  display: flex;
  align-items: center;
  gap: 14px;
}
.wizard-nav .button-primary,
.wizard-nav .button-ghost, .wizard-nav .button-outline { width: auto; margin-top: 0; }
.wizard-position { font-size: 12px; color: var(--text-muted); margin-right: auto; }
.wizard-status { font-size: 12px; color: var(--text-muted); }
.wizard-status.is-error { color: var(--rust); font-weight: 500; }

@media (max-width: 640px) {
  /* The mark alone in the header, which is what the guidelines prescribe for a
   * mobile header. Measured at 393pt: the mark at its 50px minimum plus the two
   * text lines ran the header to 421px and pushed Sign out off the screen. The
   * words are hidden from the eye but not from a screen reader, so the link
   * keeps its name. */
  .lockup-text {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .wizard-nav { position: sticky; bottom: 0; padding: 12px 0; background: var(--bg); }
  .review > div { grid-template-columns: 1fr; gap: 2px; }
  .grid-fields { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }
}

/* The confirm buttons are the destructive pair, and they get tapped on a
   phone standing in someone's kitchen. .button-ghost is 30px tall, which is
   under the 44px touch minimum — fine for a desktop toolbar, not for
   "Recalculate" next to "Cancel" where the wrong one destroys typed work. */
.confirm-actions button {
  min-height: 44px;
  padding-left: 18px;
  padding-right: 18px;
}

/* .page-head stacks on a narrow screen, and a flex column stretches its
   children — which turned the "View the list" link into a full-width box that
   reads as a card rather than a button. */
.page-head .button-ghost, .page-head .button-outline { align-self: start; width: auto; }

/* The wizard nav is sticky on a phone, so anything revealed near the bottom
   opens behind it. This reserves its height when the confirm is scrolled into
   view, so the destructive button is never the part that ends up covered. */
[data-confirm] { scroll-margin-bottom: 96px; }

@media (max-width: 640px) {
  /* And room to scroll the last card clear of the nav entirely. */
  .wizard { padding-bottom: 76px; }
}

/* Per-step tools — the supply list entry point on the Supplies step. Sits
   between the step's fields and its checklist, because it is the thing that
   step is actually for. */
.step-tool {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 14px 0 4px;
}
.step-tool .button-primary { width: auto; margin-top: 0; }
.step-tool .hint { margin: 0; }

/* ---------------------------------------------------------------------------
   Supply list
   ---------------------------------------------------------------------------
   Values here come from design-reference/Supply List.dc.html rather than from
   the shared tokens where the two differ. The prototype is the approved
   design; several of its greys and fills are one step off the token palette
   and copying the token instead is how the screen drifted the first time. */

/* chrome-spec §2: one container width for every wide screen. This was the
 * only screen that opted out, at 1220px, which put its cards 20px proud of
 * the header on either side. */
.supply-page { max-width: var(--content-max); }

/* --- Header card --------------------------------------------------------- */

.supply-header { padding: 22px 26px; }

.supply-header-top {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 20px;
  flex-wrap: wrap;
}

.supply-header-titles h1 {
  margin: 0;
  font-family: 'Libre Baskerville', Georgia, serif;
  font-weight: 400;
  font-size: 24px;
  color: var(--ink);
}
.supply-address { margin: 5px 0 0; font-size: 12.5px; color: #8A7E68; }
.supply-specs {
  margin: 8px 0 0;
  font-size: 12px;
  line-height: 1.6;
  color: #7A6E58;
  max-width: 640px;
}

.supply-header-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  flex: none;
}
.supply-header-buttons { display: flex; gap: 8px; }
.supply-header-buttons .button-primary,
.supply-header-buttons .button-ghost, .supply-header-buttons .button-outline {
  width: auto;
  margin-top: 0;
  padding: 7px 14px;
  font-size: 12.5px;
  border-radius: var(--r-md);
}
.supply-export-note {
  margin: 0;
  font-size: 10.5px;
  color: #B3A78E;
  max-width: 220px;
  text-align: right;
}

/* Warnings are rust-tinted. The observation colour (#FBF3E2) is a different
   thing and using it here made a warning look like a note. */
.supply-warnings {
  margin-top: 16px;
  padding: 12px 16px;
  background: #F3E2D9;
  border: 1px solid #E0C4B5;
  border-radius: var(--r-lg);
}
.supply-warnings p {
  display: flex;
  gap: 8px;
  align-items: baseline;
  margin: 0;
  padding: 2px 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: #6B4A3A;
}
.warn-mark { font-size: 11px; color: var(--rust); }

.supply-chips {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid #EFE6D0;
}
.stat-chip {
  padding: 4px 12px;
  border-radius: var(--r-pill);
  background: #F1E9D4;
  color: #7A6E58;
  font-size: 11.5px;
  font-weight: 600;
}
.stat-chip.is-short { background: #F3E2D9; color: var(--rust); }
.stat-chip.is-observed { background: #FBF3E2; color: #8A6A3F; }

.supply-fresh { margin: 12px 0 0; font-size: 12.5px; color: #8A7E68; }

/* --- Mobile head --------------------------------------------------------- */

.supply-mobile-head { display: none; }

/* --- Section chips ------------------------------------------------------- */
/* Completion, not scroll position: while walking a house the useful thing is
   what is left, not where you are. */

.section-chips {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin: 0;
  padding: 0;
}
.section-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 12px;
  border-radius: var(--r-pill);
  border: 1px solid #D8CDB4;
  background: #FFFDF6;
  color: var(--rust);
  font-size: 11.5px;
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
}
.section-chip.is-complete { border-color: var(--sage); background: #E7ECDF; color: #3E4A3C; }
.chip-count { opacity: 0.75; }

/* --- Sections ------------------------------------------------------------ */

/* scroll-margin-top clears the two sticky bars when a section chip jumps here:
 * the header and the crumb bar with their 1px borders, plus a little air. Read
 * from the bars' own variables rather than written as 110px, which is what it
 * still comes to. */
.supply-section {
  margin-top: 0;
  scroll-margin-top: calc(var(--header-height) + 1px + var(--crumb-height) + 1px + 16px);
}

.section-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 0 4px 8px;
}
.section-title {
  font-size: 11px;
  letter-spacing: 2px;
  font-weight: 600;
  color: var(--rust);
  text-transform: uppercase;
}
.section-count { font-size: 11px; color: #8A7E68; }

.section-card { padding: 0; overflow: hidden; border-radius: var(--r-xl); }

/* --- The grid ------------------------------------------------------------ */
/* Six columns: item, required, one per unit, need to order, derivation,
   observation. The observation is a column and not a drawer — someone typing
   a note while holding a tape measure should not have to open anything. */

.supply-grid { --unit-count: 1; }

.grid-head,
.grid-row {
  display: grid;
  grid-template-columns:
    minmax(0, 1.5fr) 76px repeat(var(--unit-count), 96px) 96px minmax(0, 1.1fr) minmax(0, 1.4fr);
  gap: 12px;
  align-items: center;
  padding: 9px 18px;
}

.grid-head {
  background: #FBF7EC;
  border-bottom: 1px solid #EBE1C9;
}
.grid-head span {
  font-size: 10px;
  letter-spacing: 1.2px;
  color: #A79B85;
  font-weight: 600;
}
.grid-head .num { text-align: right; }

.grid-row { border-bottom: 1px solid #F1E9D6; }
.grid-row:last-child { border-bottom: none; }

.cell-item { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.item-name { font-size: 13px; font-weight: 500; color: var(--ink); }
.confirm-mark { font-size: 10px; color: var(--rust); font-weight: 600; }
.mobile-req { display: none; }

.cell-req { font-size: 13px; font-weight: 600; color: var(--forest); text-align: right; }
.cell-req.is-dash { color: #C9BCA0; }

/* The wrapper exists for the phone, where the per-unit fields sit side by
   side. display:contents keeps them as real grid columns here. */
.onhand-group { display: contents; }
.cell-onhand { text-align: right; }
.unit-label { display: none; }

.on-hand {
  width: 100%;
  padding: 7px 9px;
  font-family: inherit;
  font-size: 12.5px;
  text-align: right;
  color: var(--ink);
  background: #FBF7EC;
  border: 1px solid #E3D9C2;
  border-radius: 6px;
  outline: none;
  min-width: 0;
}
.on-hand:focus-visible { border-color: var(--rust); }

.cell-need { font-size: 12.5px; font-weight: 600; text-align: right; }
.cell-need .is-dash { color: #C9BCA0; }
.cell-need .needs { color: var(--rust); }
.cell-need .covered { color: #5C6B58; }

.cell-deriv {
  font-size: 11.5px;
  line-height: 1.5;
  color: #A79B85;
  font-style: italic;
}

/* The observation earns a warm fill once it has something in it, so a row
   carrying a note is visible while scrolling past. */
.observation {
  width: 100%;
  padding: 7px 9px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 400;
  color: var(--rust);
  background: transparent;
  border: 1px solid #EFE6D0;
  border-radius: 6px;
  outline: none;
  min-width: 0;
}
.observation.has-value { border-color: #DCC69E; background: #FBF3E2; font-weight: 500; }
.observation-mobile,
.observation-toggle { display: none; }

/* --- Additions ----------------------------------------------------------- */

.additions-card { padding: 16px 20px; }
.additions-empty { margin: 0; font-size: 12.5px; color: #B3A78E; }
.additions-hint { display: none; }

.additions { list-style: none; margin: 0; padding: 0; }
.additions li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 0;
  border-bottom: 1px solid #F1E9D6;
  font-size: 13px;
  color: #312A1E;
}
.additions li.is-pending { opacity: 0.55; }
.addition-mark { font-size: 11px; color: var(--rust); }
.addition-text { flex: 1; }
.addition-remove {
  border: none;
  background: transparent;
  color: #B3A78E;
  font-size: 14px;
  cursor: pointer;
  padding: 2px 6px;
}

.addition-add { display: flex; gap: 8px; margin-top: 12px; max-width: 520px; }
.addition-add input {
  flex: 1;
  min-width: 0;
  padding: 8px 11px;
  font-family: inherit;
  font-size: 12.5px;
  color: var(--ink);
  background: #FBF7EC;
  border: 1px solid #D8CDB4;
  border-radius: var(--r-md);
  outline: none;
}
.addition-add .button-primary {
  width: auto;
  margin-top: 0;
  padding: 8px 14px;
  font-size: 12.5px;
  border-radius: var(--r-md);
}

/* The sticky bar is the phone's way of adding something. On a desktop the
   field sits in the additions card where it can simply be seen. */
.supply-sticky { display: none; }

/* --- Sticky bar ---------------------------------------------------------- */

.supply-sticky {
  position: sticky;
  bottom: 0;
  margin-top: 22px;
  background: #FBF7EC;
  border-top: 1px solid #E3D9C2;
  padding: 12px 16px;
}
.sticky-add { display: flex; gap: 8px; margin-bottom: 10px; }
.sticky-add input {
  flex: 1;
  min-width: 0;
  padding: 12px;
  font-family: inherit;
  font-size: 14px;
  color: var(--ink);
  background: #FFFDF6;
  border: 1px solid #D8CDB4;
  border-radius: var(--r-lg);
  outline: none;
}
.sticky-add .button-primary { width: auto; margin-top: 0; padding: 12px 16px; border-radius: var(--r-lg); }
.sticky-row { display: flex; align-items: center; gap: 10px; }
.sticky-status { flex: 1; font-size: 11.5px; color: #8A7E68; }
.supply-sticky .button-ghost, .supply-sticky .button-outline {
  width: auto;
  padding: 10px 14px;
  border-radius: var(--r-lg);
  font-size: 12.5px;
}

/* --- Below 1000px the table stops being a table ---------------------------
   Not 820. At 820 the six-column grid leaves the derivation column 122px,
   which wraps "4 per queen/full/pull-out · 2 per twin" onto four lines, and
   the item column 166px, which does the same to the longer kitchen names.
   Cards start earlier rather than shipping a squeezed table. */

@media (max-width: 1000px) {
  .supply-header { display: none; }

  .supply-mobile-head { display: block; }
  .mobile-property { font-family: 'Libre Baskerville', Georgia, serif; font-size: 19px; }
  .mobile-progress { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
  .mobile-track {
    flex: 1;
    height: 6px;
    background: #EBE1C9;
    border-radius: 3px;
    overflow: hidden;
  }
  .mobile-fill { height: 100%; background: var(--rust); border-radius: 3px; }

  .section-chips { margin-top: 12px; }

  /* Serif, rust, and a heavier rule than the desktop label. */
  .supply-section { margin-top: 26px; scroll-margin-top: 12px; }
  .section-head {
    padding: 0 0 7px;
    border-bottom: 2px solid #E3D9C2;
  }
  .section-title {
    font-family: 'Libre Baskerville', Georgia, serif;
    font-size: 17px;
    font-weight: 400;
    letter-spacing: 0;
    text-transform: none;
    color: var(--rust);
  }
  .section-count { flex: none; }

  .section-card { background: none; border: none; border-radius: 0; overflow: visible; }
  .grid-head { display: none; }

  /* Each row becomes its own card. The left edge turns sage once the row has
     been counted, so what is left shows at a glance while scrolling. */
  .grid-row {
    display: flex;
    flex-direction: column;
    /* The desktop rule centres cells in their grid track. In a column flex
       that centres them horizontally, which is not what it means here. */
    align-items: stretch;
    padding: 14px 16px;
    margin-top: 10px;
    background: #FFFDF6;
    border: 1px solid #E8DEC7;
    border-left: 3px solid #E8DEC7;
    border-radius: var(--r-xl);
  }
  .grid-row.is-filled { border-left-color: var(--sage); }

  .cell-item {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 10px;
    flex-wrap: nowrap;
  }
  .item-name { font-size: 14.5px; }

  /* Required moves into a pill beside the name; the desktop column goes. */
  .cell-req { display: none; }
  .mobile-req {
    display: inline-block;
    flex: none;
    padding: 3px 10px;
    border-radius: var(--r-pill);
    border: 1px solid #E3D9C2;
    background: transparent;
    color: #A79B85;
    font-size: 10.5px;
    font-weight: 600;
    white-space: nowrap;
  }
  .mobile-req.is-calc { background: #EDE1C8; border-color: transparent; color: #7A5B33; }

  /* The prototype reads item, derivation, inputs, need, observation. The
     desktop column order is different, so it is set explicitly here rather
     than left to the order the markup happens to be in. */
  .cell-item { order: 1; }
  .cell-deriv { order: 2; display: block; margin-top: 2px; font-size: 11px; }
  .onhand-group { order: 3; display: flex; gap: 8px; margin-top: 10px; }
  .cell-need { order: 4; }
  .cell-obs { order: 5; }

  .cell-onhand { flex: 1; min-width: 0; display: block; text-align: left; }
  .unit-label {
    display: block;
    margin-bottom: 4px;
    font-size: 9.5px;
    letter-spacing: 1px;
    color: #A79B85;
    font-weight: 600;
  }
  .on-hand {
    padding: 12px;
    font-size: 15px;
    text-align: left;
    border-radius: var(--r-lg);
    border-color: #D8CDB4;
  }

  .cell-need { display: block; margin-top: 8px; font-size: 13px; text-align: left; }
  .cell-need .is-dash { display: none; }

  .cell-obs { display: block; }
  .observation { display: none; }
  .observation-mobile {
    display: block;
    width: 100%;
    min-height: 56px;
    margin-top: 10px;
    padding: 10px 12px;
    font-family: inherit;
    font-size: 13.5px;
    line-height: 1.5;
    color: var(--ink);
    background: #FBF3E2;
    border: 1px solid #DCC69E;
    border-radius: var(--r-lg);
    outline: none;
    resize: vertical;
  }
  .observation-mobile[hidden] { display: none; }
  .observation-toggle {
    display: inline-block;
    margin-top: 8px;
    padding: 8px 12px;
    font-family: inherit;
    font-size: 12px;
    font-weight: 500;
    color: var(--rust);
    background: transparent;
    border: 1px dashed #C9BCA0;
    border-radius: var(--r-lg);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }

  .additions-card { padding: 0; background: none; border: none; }
  .additions-empty { display: none; }
  .additions-hint { display: block; margin: 4px 0 0; font-size: 11.5px; color: #8A7E68; }
  .addition-add { display: none; }

  .additions li {
    gap: 10px;
    margin-top: 8px;
    padding: 12px 14px;
    background: #FFFDF6;
    border: 1px solid #E8DEC7;
    border-left: 3px solid var(--rust);
    border-radius: var(--r-lg);
    font-size: 14px;
    line-height: 1.4;
  }
  .addition-mark { display: none; }
  .addition-remove { font-size: 16px; padding: 4px 8px; }

  .supply-sticky { display: block; }

  /* No bottom padding. The sticky bar is .supply-page's last child and sits in
     normal flow, so the last card clears it without any help — measured +20px
     at full scroll on a 97-item sheet, with the add row open or shut. The 110px
     that used to be here cleared nothing; it just left a strip of body cream
     under the bar's #FBF7EC at the end of every scroll. */
  .supply-page { padding-bottom: 0; }
  .supply-sticky {
    margin: 0 calc(var(--gutter) * -1);
    padding: 12px 16px 26px;
  }
}

/* ---------------------------------------------------------------------------
   Users
   --------------------------------------------------------------------------- */

/* .appbar-nav and its items are defined with the header, chrome-spec §1.
 * The colour-only :hover that used to live here is gone with them. */

.people { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }

.person {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.3fr) auto;
  align-items: center;
  gap: 16px;
  padding: 14px 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
}
/* Greyed, not hidden. They are still part of the record. */
.person.is-inactive { opacity: 0.62; }

.person-who { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.person-name { font-size: 14px; font-weight: 500; color: var(--text); }
.person-email {
  font-size: 12px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
}

.person-state { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.person-detail { font-size: 11.5px; color: var(--text-muted); }

.state-pill {
  align-self: start;
  padding: 3px 10px;
  border-radius: var(--r-pill);
  font-size: 11px;
  font-weight: 600;
}
.state-pill.is-on { background: #DDE3D6; color: #3E4A3C; }
.state-pill.is-off { border: 1px dashed var(--border-strong); color: var(--text-faint); }
.state-pill.is-pending { background: #EDDFC3; color: #6B4A23; }

.person-actions { display: flex; justify-content: flex-end; }
.person-actions .button-ghost,
.person-actions .button-outline,
.person-actions .button-danger-ghost { width: auto; min-height: 40px; }
.person-you { font-size: 11.5px; color: var(--text-faint); }

.people-note { margin: 0; }
.people-note[data-error] { color: var(--rust); font-weight: 500; }

@media (max-width: 640px) {
  .person { grid-template-columns: 1fr; gap: 10px; }
  .person-actions { justify-content: flex-start; }
  .person-actions .button-ghost,
  .person-actions .button-outline,
  .person-actions .button-danger-ghost { min-height: 44px; }
  .appbar-nav { margin-left: 14px; padding-left: 14px; }
}

.invite-card { margin-bottom: 0; }
.invite-form {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr) auto;
  align-items: end;
  gap: 12px;
}
.invite-form .field { margin: 0; }
.invite-form .button-primary { width: auto; margin: 0; min-height: 40px; }
.invite-result { margin: 12px 0 0; word-break: break-all; }
.invite-result.is-error { color: var(--rust); font-weight: 500; }

@media (max-width: 640px) {
  .invite-form { grid-template-columns: 1fr; }
  .invite-form .button-primary { width: 100%; min-height: 44px; }
}

/* The invite and reset pages stand alone — no app bar, no navigation. */
.auth-page { max-width: 460px; padding-top: 64px; }
.auth-card { padding: 28px; }
.auth-title { margin: 0 0 4px; font-size: 20px; font-weight: 400; }
.auth-org { font-size: 10px; letter-spacing: 2.5px; color: var(--text-muted); margin-left: 6px; }

.login-forgot { margin: 12px 0 0; text-align: center; }
.person-actions { gap: 8px; }

/* Owner export control. A menu rather than two buttons, because the choice is
   the same document in two shapes and the note under it is the part that
   matters — what an owner will and will not see. */
/* The punch list's header pair.
 *
 * .head-actions had no rule at all, so the two controls sat in a plain block
 * <div>: full width, stacked, and touching. They were also the same outline
 * button, which said they were the same kind of thing. They are not — Export
 * is something you do to this launch, Settings is somewhere you go — so
 * Export keeps the Outline weight and Settings drops to Ghost, the same
 * treatment Settings already has on the portfolio row.
 *
 * 10px is the gap .actions uses for every other button pair in the app. */
.head-actions { display: flex; align-items: flex-start; gap: 10px; }

.export-menu { position: relative; }
.export-options {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 20;
  width: 260px;
  padding: 8px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  box-shadow: 0 8px 24px rgba(36, 27, 18, 0.10);
}
/* Hung from the button's left edge instead, when hanging from its right would
 * run off the screen. Set by public/punch.js, which measures on opening. */
.export-options.export-options-start { left: 0; right: auto; }
.export-options a {
  display: block;
  padding: 9px 10px;
  font-size: 13px;
  color: var(--rust);
  text-decoration: none;
  border-radius: var(--r-md);
}
.export-options a:hover { background: var(--surface-tint); text-decoration: none; }
.export-note {
  margin: 6px 4px 2px;
  font-size: 11px;
  line-height: 1.45;
  color: var(--text-muted);
  border-top: 1px solid var(--border-soft);
  padding-top: 8px;
}

@media (max-width: 640px) {
  .export-options { width: min(280px, calc(100vw - 32px)); }
  .export-options a { min-height: 44px; display: flex; align-items: center; }
}


/* Packages on the punch list ----------------------------------------------
 *
 * A package block is the package row plus the items it holds, kept together:
 * sorting reorders inside a block and among the loose rows, never splits one.
 * The row uses the table's own grid so its cells line up with the head above
 * it — a package that did not line up would read as a different table. */
.package-block {
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  margin-bottom: 8px;
  overflow: hidden;
}
/* 12px between the last block and the first loose row, so the two kinds of
 * thing are visibly different without a heading saying so. */
.package-block:last-of-type { margin-bottom: 12px; }

.package-row {
  position: relative;
  display: grid;
  grid-template-columns: 34px 22px minmax(0, 1fr) 116px 104px 96px 118px 108px;
  gap: 10px;
  align-items: center;
  padding: 10px 18px;
  min-height: 52px;
  cursor: pointer;
  list-style: none;
}
.package-row::-webkit-details-marker { display: none; }
.package-row:hover { background: #F7F0DD; }

/* The caret is the affordance, and it turns rather than swapping glyph: one
 * element, no reflow, and it reads as the same control in both states. */
.package-caret {
  display: inline-block;
  width: 0; height: 0;
  margin-left: 6px;
  border-left: 6px solid var(--rust);
  border-top: 4.5px solid transparent;
  border-bottom: 4.5px solid transparent;
  transition: none;
}
.package-block[open] .package-caret { transform: rotate(90deg); }

.package-title { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* THE WHOLE ROW IS THE DOOR, on a desktop. It was the name alone — a 14px
 * target in a 1200px row, in a list where every other row opens its item from
 * anywhere on it. The overlay is a.col-title's, exactly: an ::after at inset 0
 * against the row, with the one control that is not the drawer raised above
 * it. Here that control is the caret, because expanding and opening are
 * different questions and the row has to answer both. */
.package-name {
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
}
.package-name::after { content: ''; position: absolute; inset: 0; z-index: 1; }
.package-name:focus-visible::after { outline: 2px solid var(--rust); outline-offset: -2px; }
/* THE CARET IS NOW THE ONLY WAY TO EXPAND, so its cell has to be a target
 * rather than the size of a 6px triangle. It was 34x21 inside a 61px row —
 * decorative was fine while the whole summary toggled, and is not fine now.
 * align-self overrides the row's align-items: center, which is what was
 * shrinking it to its contents. */
.package-row .col-check {
  position: relative;
  z-index: 2;
  align-self: stretch;
  display: flex;
  align-items: center;
  cursor: pointer;
}
/* Two boxes, as everywhere else: 6px of visible caret, 44 of target. The cell
 * itself cannot simply be made wider — it is a grid column the table header
 * lines up with — so the target reaches past it. */
.package-row .col-check::after { content: ''; position: absolute; inset: -2px -5px; }
/* The name underlines from anywhere on the row, so the hover says which of the
 * two things a click in open space would do. */
.package-row:hover .package-name { text-decoration: underline; }
.package-row .col-check:hover ~ .col-title .package-name { text-decoration: none; }
.package-meta { font-size: 11.5px; color: var(--text-muted); }

.package-total { font-size: 13px; font-weight: 600; color: var(--ink); display: block; }
.package-total-note,
.package-quotes-in { font-size: 11px; color: var(--text-muted); }

/* The package states (§0, plus E2's three). Every one is an existing recipe — a
 * pill at the same size and radius as every other pill in the app, and every
 * colour is already in tokens.md.
 *
 * THE LOGIC IS tokens.md's OWN: neutral, warming through the clay family as
 * work is committed, forest when it is done, rust only for a refusal. E2's
 * three continue it rather than starting a fourth family —
 *
 *   In progress        sage outline. We are waiting on the vendor, and the
 *                      outline group is the waiting group (see below).
 *   Ready for review   the clay fill, which `Awarded` also wears, because both
 *                      mean the same thing to whoever is scanning the column:
 *                      something has landed and the next move is ours.
 *   Complete           forest, which tokens.md gives for Complete and Done.
 */
.package-pill-draft { color: var(--text-muted); border-color: #D8CDB6; }
/* OUTLINE, NOT FILLED, and not only because it was painted --surface-tint and
 * the package block is now that colour — it would have vanished, but the
 * reason it changes shape is the one the rest of this row of rules already
 * follows. The filled pills are the ones where a fact has landed: quotes are
 * in, a vendor was awarded, the owner answered. The outline ones are the ones
 * where we are waiting: nothing drafted yet, sent to the owner, handed off.
 *
 * `Out for quote` is waiting on a vendor. It sat in the filled group on its
 * own, which is why it needed the palette's lightest fill to stay quiet
 * enough — a fill doing an outline's job.
 *
 * It takes `Quotes in`'s own ink and border and leaves the fill behind, so
 * the two read as one stage family with the fill arriving when the quotes do.
 * The neutral outline was tried first and made it a twin of `Draft`, which
 * are the two states a package spends most of its life in and the two a
 * colour should therefore tell apart. */
.package-pill-out_for_quote { color: #6B4A23; border-color: #DCC69E; }
.package-pill-quotes_in { background: #EDDFC3; color: #6B4A23; border-color: #DCC69E; }
.package-pill-awarded { background: #E5CFA6; color: #5F421D; }
.package-pill-sent_to_owner { color: #8A6A3F; border-color: #D9C3A2; }
.package-pill-approved { background: #DDE3D6; color: #3E4A3C; }
.package-pill-declined { background: #EAD8D1; color: var(--rust); }
.package-pill-needs_requote { background: #EAD8D1; color: var(--rust); }
/* Was .package-pill-handed_off, which nothing ever rendered: it was written for
 * a column that existed from migration 011 and had no stage behind it until
 * E2. Same two colours, now on the stage that means what that name meant. */
.package-pill-in_progress { color: #5C6B58; border-color: #B8C2AE; }
.package-pill-ready_for_review { background: #E5CFA6; color: #5F421D; }
.package-pill-complete { background: var(--forest); color: #F3EEE0; }

/* Governed, not set (§5.1): the package decides this item's status, so the pill
 * is a statement. Nothing about it invites a click. */
.pill.is-governed { cursor: default; }

/* Loud until resolved (§2.4). A withdrawn figure that nobody has replaced is
 * not a state to leave reading like any other. */
.package-callout {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 16px;
  background: #EAD8D1;
  color: var(--rust);
  font-size: 13px;
}
.package-callout .package-requote { color: var(--rust); border-color: #D9C3B8; }

/* The items are the package's, not the list's. They used to sit on exactly
 * the list's own surface, which is why a packaged row and a loose row looked
 * identical once your eye had left the parent. See the spine below. */
.package-items { background: inherit; }
/* Nested rows are indented in the item column alone, so every other cell stays
 * in the column it belongs to. */
.punch-row.is-packaged .col-title { padding-left: 28px; }
.punch-row.is-packaged { border-bottom-color: #EFE6D0; }
.cost-pending { font-size: 12px; font-style: italic; color: var(--text-muted); }

/* The package drawer ------------------------------------------------------ */
.package-drawer-status { margin: 4px 0 0; }
.package-item-list, .package-vendor-list { list-style: none; margin: 0; padding: 0; }
.package-item, .package-vendor {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border-soft);
  font-size: 13px;
}
.package-item-title, .package-vendor-name { font-weight: 500; color: var(--ink); }
.package-item-room, .package-vendor-state { font-size: 11.5px; color: var(--text-muted); }
.package-item-remove, .package-copy { margin-left: auto; }
/* From handoff on, every item row carries the vendor's status (E2), and that
 * is the cell that belongs at the right-hand edge — Remove follows it rather
 * than taking the whole gap for itself. Two auto margins in one flex row split
 * the space between them, which left the status floating in the middle. */
.package-item-status { margin-left: auto; }
.package-item-list.is-released .package-item-remove { margin-left: 8px; }
.package-vendor.is-retired { opacity: 0.55; }
.package-item.is-empty, .package-vendor.is-empty { color: var(--text-muted); font-style: italic; }
.package-add-vendor { margin: 10px 0; }
.package-drawer-foot { margin-top: 18px; }

/* The status an item cannot change here, and where it can be changed. */
.package-governed { background: var(--surface-tint); border-radius: var(--r-md); padding: 12px; }
.package-governed-text { margin: 4px 0 8px; font-size: 13px; color: var(--ink); }

/* §5.2's cost, read-only. The figure carries the weight; the line under it says
 * where the figure comes from.
 *
 * Neither uses --text-muted, which measures 3.29:1 on this tinted ground and so
 * fails the 4.5 body floor — the same trap as the owner-page grey. Secondary
 * here is smaller, not fainter: --forest is 10.09:1. */
.package-governed-cost { margin: 4px 0 2px; font-size: 15px; font-weight: 600; color: var(--ink); }
.package-governed-cost.cost-pending { font-size: 13px; font-weight: 400; color: var(--forest); }
.package-governed-note { margin: 0; font-size: 12px; color: var(--forest); }

/* Naming a package (§2.3). A modal on a wide screen, a sheet on a phone. */
.package-dialog-scrim { position: fixed; inset: 0; background: rgba(36, 27, 18, 0.18); z-index: 29; }
.package-dialog {
  position: fixed;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  z-index: 30;
  width: 480px;
  max-width: calc(100vw - 32px);
  max-height: calc(100vh - 64px);
  overflow-y: auto;
  padding: 24px;
  background: var(--surface);
  border-radius: var(--r-xl);
  box-shadow: 0 8px 24px rgba(36, 27, 18, 0.10);
}
.package-dialog:focus { outline: none; }
.package-dialog-title { margin: 0 0 14px; font-family: 'Libre Baskerville', Georgia, serif; font-size: 18px; font-weight: 400; }
.package-dialog-label { margin: 14px 0 6px; }
.package-dialog-items { list-style: none; margin: 0; padding: 0; font-size: 13px; }
.package-dialog-items li { padding: 4px 0; border-bottom: 1px solid var(--border-soft); }
.package-dialog-items.is-muted li { color: var(--text-muted); font-size: 12px; }

/* The items that will not be added. A warning band rather than a third small
   grey heading, which read as another section with its items included. */
.package-dialog-warning {
  margin: 14px 0 4px;
  padding: 10px 12px;
  background: #EAD8D1;
  border-radius: var(--r-md);
  color: var(--rust);
}
.package-dialog-warning-head { margin: 0 0 4px; font-size: 13px; font-weight: 600; }
.package-dialog-warning .package-dialog-items li { border-bottom-color: #D9C3B8; font-size: 12.5px; }
.package-dialog-warning .package-dialog-items li:last-child { border-bottom: none; }

/* The package drawer (§2.1, §2.2) ---------------------------------------------
 *
 * One drawer now, so it has two widths: 520 while Quotes is closed and 820
 * (the old compare width) while it is open. The transition is on width alone
 * — the drawer already slides in, and animating both reads as a stutter. */
.drawer.package-drawer { width: 520px; transition: width 180ms ease-out; }
.drawer.package-drawer.is-wide { width: 820px; }

.package-drawer-head { align-items: center; gap: 8px; }
.package-drawer-name {
  margin: 0;
  font-family: 'Libre Baskerville', Georgia, serif;
  font-size: 20px; font-weight: 400; color: var(--text);
}
/* §2.2: 32px visual, 44px hit area. Those are two different boxes and it was
 * one — a 44px square of ink beside a 20px heading, which read as a button
 * rather than an affordance on the name. The square stays 32; the pseudo
 * element carries the other 12 so a thumb still has 44 to land on. */
.package-rename {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; min-width: 0; min-height: 0; padding: 0;
  font-size: 15px; color: var(--text-muted);
}
.package-rename::after { content: ''; position: absolute; inset: -6px; }
.package-rename:hover { color: var(--rust); }
.package-drawer-rename-input { font-size: 16px; flex: 1; }

/* The outcome block: the answer, before anything that has to be opened. */
.outcome { border-radius: 10px; padding: 20px; margin-bottom: 4px; }

/* THE FOUR BACKGROUNDS ARE §2.4'S COLUMN, and the names are what each one
 * means rather than what it looks like: calm while we are waiting on somebody,
 * firm once there is something to act on, approved for the one green state,
 * and alarm for a price somebody agreed to that no longer holds. */
.outcome-calm { background: #F1E9D4; }
.outcome-firm { background: #EDDFC3; }
.outcome-alarm { background: #EAD8D1; }
.outcome-declined { background: #EAD8D1; }
.outcome-approved { background: #DDE3D6; }
/* THE SAME GREEN AS APPROVED, AND ITS OWN NAME. The block's background says
 * what register the news is in, and "the owner agreed to pay" and "we accepted
 * the work" are both settled good news — so they share it. They are not the
 * same event, which is why the key is separate rather than `approved` being
 * reused: the pill above it is forest, the headline names who signed off, and
 * if either of those two states ever needs its own shade there is somewhere to
 * put it. */
.outcome-complete { background: #DDE3D6; }

.outcome-headline {
  margin: 10px 0 0;
  font-family: 'Libre Baskerville', Georgia, serif;
  font-size: 20px; line-height: 1.3; color: var(--text);
}
/* §2.4 names a headline colour for exactly two stages, and these are they.
 * The withdrawn rows share declined's background and are deliberately NOT
 * given its headline colour: the tint says something is wrong, the facts line
 * under it says what, and colouring the headline as well would make the three
 * rust states indistinguishable from the one the owner actually settled. */
.outcome-approved .outcome-headline,
.outcome-complete .outcome-headline { color: #2E382D; }
.outcome-declined .outcome-headline { color: var(--rust); }

/* §2.2: Rubik 13 #7A6E58 — which is --text-secondary, not --text-muted. The
 * two differ by a shade and this line sits directly under a 20px serif
 * headline, where the lighter grey read as disabled rather than quiet. */
.outcome-facts { margin: 6px 0 0; font-size: 13px; line-height: 1.5; color: var(--text-secondary); }
.outcome-approved .outcome-facts,
.outcome-complete .outcome-facts { color: #3E4A3C; }

/* §2.2: 16px below the facts line, at its natural width. Buttons in this app
 * are width:100% by default, which made the one action in the drawer a bar
 * across the block — reading as the whole of it rather than the one thing
 * offered under a statement. */
.outcome-action { margin-top: 16px; }
.outcome-action .button-primary { width: auto; }

/* The other answer at `ready_for_review` (§2.4's "at most one action" is about
 * the primary — see secondaryFor in src/packageCopy.js). A text button, under
 * the block's own action, with the sentence that says what it costs: this is
 * the control that reaches a vendor, and it should take a moment to press.
 *
 * NOT inside .outcome-action, which the phone hides in favour of the sticky
 * bar. Disagreeing must not be the one thing a phone cannot do. */
.outcome-secondary {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  margin-top: 12px;
}
.outcome-send-back { color: var(--rust); font-size: 13px; }
.outcome-secondary .hint { font-size: 11.5px; }

/* Section headers: 52px, the whole row clickable. */
/* §2.1: 1px #E6DCC4 with 20px above and below. The 20px above comes from the
 * body's own padding; the 20px below was 16px inherited from a `details` rule
 * written for something else, which is the kind of spacing that is right by
 * accident until the day that other rule changes. */
.drawer-section { border-top: 1px solid #E6DCC4; padding-top: 20px; }
.drawer-section:first-of-type { border-top: none; }
.drawer-section-head {
  display: flex; align-items: center; gap: 10px;
  min-height: 52px; padding: 0 2px;
  cursor: pointer; list-style: none;
}
.drawer-section-head::-webkit-details-marker { display: none; }
.drawer-section-head::before {
  content: '\25B8'; color: var(--rust); font-size: 11px; width: 10px;
}
.drawer-section[open] > .drawer-section-head::before { content: '\25BE'; }
.drawer-section-label {
  font-family: Rubik, system-ui, sans-serif;
  font-size: 14px; font-weight: 600; color: var(--text);
}
.drawer-section-summary {
  margin-left: auto; font-size: 13px; color: var(--text-muted); text-align: right;
}
.drawer-section-body { padding: 4px 0 20px; }
.package-drawer-foot { margin-top: 20px; padding-top: 16px; border-top: 1px solid #E6DCC4; }
.package-item { position: relative; }

@media (max-width: 900px) {
  .drawer.package-drawer, .drawer.package-drawer.is-wide { width: 100%; }
}

/* The package row's sub-line and child cells (§3.1, §3.2) --------------------
 *
 * The sub-line turns rust only where the package is stuck and needs us: a
 * revised estimate is needed, the estimate lapsed, or an approval was
 * withdrawn. Waiting on a vendor is not stuck — if everything that is merely
 * in progress is coloured, the colour stops meaning anything. */
.package-meta.is-stuck { color: var(--rust); }
.package-total.is-muted { color: #A79B85; }

/* The child cell that used to repeat the parent's pill on every row. */
.child-status { font-size: 12px; color: #A79B85; }
.child-status.is-open { color: var(--text-muted); }
.child-status.is-settled { color: #A79B85; }

/* The demotions (§2.6, §2.7, §2.8) -------------------------------------------
 *
 * Each of these stays available and stops being the point. They are ghost and
 * text treatments rather than removals: the difference between "you can" and
 * "you should", which is the whole of this ticket. */

/* §2.6's row menu: 32px of visible dots, 44px of target. Same two-box rule as
 * the rename pencil — the dots are small because they are not the point; the
 * target is 44 because a thumb still has to land on it. */
.row-menu { position: relative; }
.row-menu-toggle {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  cursor: pointer; list-style: none;
  color: var(--text-muted); font-size: 15px; line-height: 1;
  border-radius: var(--r-sm);
}
.row-menu-toggle::-webkit-details-marker { display: none; }
.row-menu-toggle::after { content: ''; position: absolute; inset: -6px; }
.row-menu-toggle:hover { color: var(--rust); background: #F1E9D6; }
.row-menu[open] > .row-menu-toggle { color: var(--rust); background: #F1E9D6; }
.row-menu-items {
  position: absolute; right: 0; top: calc(100% + 4px); z-index: 5;
  min-width: 140px; padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border-strong); border-radius: var(--r-md);
  box-shadow: 0 8px 24px rgba(36, 27, 18, 0.10);
}
.row-menu-items .linkish {
  display: flex; align-items: center;
  width: 100%; min-height: 44px; padding: 0 8px;
  font-size: 13px; color: var(--rust); text-align: left;
}

/* §2.7: still possible, no longer the point. */
.add-vendor-ghost {
  display: inline-flex; align-items: center;
  min-height: 44px;
  font-family: Rubik, system-ui, sans-serif;
  font-size: 13px; font-weight: 500; color: var(--rust);
}

/* §2.8: a text button at the foot, with a 44px band to press. */
.package-delete {
  display: inline-flex; align-items: center;
  min-height: 44px;
  font-family: Rubik, system-ui, sans-serif;
  font-size: 13px; font-weight: 500; color: var(--rust);
}

/* The Quotes section's two modes (§2.3, §2.9) --------------------------------
 *
 * Record mode is the same grid, read rather than used: the awarded row is
 * marked, the others go quiet, and no row carries a control. Nothing is
 * hidden — it is a record of a decision, so it all shows when the section is
 * expanded. Only the buttons go. */
.compare-totals.is-record .compare-total-row.is-awarded { background: #DDE3D6; border-radius: 7px; }
.compare-totals.is-record .compare-total-row:not(.is-awarded) .compare-vendor-name,
.compare-totals.is-record .compare-total-row:not(.is-awarded) .compare-total {
  color: var(--text-muted);
}

/* The way back into the decision: one text button under the section, never
 * beside a row. Beside a row it reads as "change to this one"; here it reads
 * as reopening the decision, which is what it does. */
.compare-change { margin-top: 14px; }
.compare-change-award {
  display: inline-flex; align-items: center;
  min-height: 44px;
  font-family: Rubik, system-ui, sans-serif;
  font-size: 13px; font-weight: 500; color: var(--rust);
}

/* Reopened: the band says what taking the new award will cost, and sits above
 * the rows it has just given buttons back to. */
.reopen-band {
  display: flex; align-items: center; gap: 12px;
  margin-bottom: 12px; padding: 10px 12px;
  background: #EDDFC3; border-radius: 7px;
  font-size: 13px; line-height: 1.5; color: #6B4A23;
}
.reopen-band-text { flex: 1; }
.reopen-band .button-ghost { flex: none; min-height: 44px; }

@media (max-width: 900px) {
  /* §2.9: a 44px ghost band under the totals card, full width, because a text
   * button at the foot of a card is a 13px target on a phone. */
  .compare-change { margin-top: 10px; }
  .compare-change-award {
    display: flex; width: 100%; justify-content: center;
    border: 1px solid var(--border-strong); border-radius: 7px;
  }
  .reopen-band { flex-wrap: wrap; }
  .reopen-band .button-ghost { width: 100%; }
}

/* The confirm-with-consequence dialog (§2.4) ---------------------------------
 *
 * The package dialog's shell, deliberately: same scrim, same z-indices, same
 * centred box, so there is one dialog in this app rather than two that drift.
 * What differs is that this one is read rather than filled in, so it is sized
 * to be read at a glance and never scrolled.
 *
 * NO max-height AND NO overflow HERE, unlike .package-dialog. That is not an
 * oversight. A scrollbar on this dialog means a consequence is hidden, which
 * is the defect it was built to fix — the browser alert it replaced put the
 * owner's approval below its own fold. If something stops fitting, the words
 * get shorter; a test asserts the content is shorter than the box. */
.confirm-dialog-scrim { position: fixed; inset: 0; background: rgba(36, 27, 18, 0.18); z-index: 29; }
.confirm-dialog {
  position: fixed;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  z-index: 30;
  width: 460px;
  max-width: calc(100vw - 32px);
  padding: 24px;
  background: var(--surface);
  border-radius: var(--r-xl);
  box-shadow: 0 8px 24px rgba(36, 27, 18, 0.10);
}
.confirm-dialog:focus { outline: none; }
.confirm-dialog-title {
  margin: 0 0 14px;
  font-family: 'Libre Baskerville', Georgia, serif;
  font-size: 18px; font-weight: 400; line-height: 1.35;
}
.confirm-dialog-points { list-style: none; margin: 0; padding: 0; }
.confirm-dialog-points li {
  padding: 10px 0;
  border-top: 1px solid var(--border-soft);
  font-size: 13.5px; line-height: 1.55; color: var(--text);
}
/* The one that costs the most, first and in the colour this app uses for it.
   It is first in the DOM too — the order is not a visual trick. */
.confirm-dialog-points li.is-owner,
/* The one that cannot be taken back, last and in the same weight. It is last
   in the DOM too: it is the line read on the way to the button. */
.confirm-dialog-points li.is-grave { color: var(--rust); font-weight: 500; }
.confirm-dialog-actions { margin-top: 18px; }

@media (max-width: 900px) {
  /* A sheet at the foot of the screen, where a thumb is — the bulk sheet's
     shape, for the same reason. Full width, square at the bottom. */
  .confirm-dialog {
    top: auto; bottom: 0; left: 0;
    transform: none;
    width: auto; right: 0;
    max-width: none;
    padding: 16px 16px calc(16px + env(safe-area-inset-bottom, 0px));
    border-radius: var(--r-xl) var(--r-xl) 0 0;
  }
  /* Full-width stacked buttons, the destructive one under the thumb's reach
     but not where a mis-tap lands first. */
  .confirm-dialog-actions { display: flex; flex-direction: column-reverse; gap: 8px; }
  /* The anchor too: "Go to punch list" is a link wearing a button, and a
     selector that said only `button` left it 118px wide in a column of
     full-width ones. */
  .confirm-dialog-actions button,
  .confirm-dialog-actions a {
    width: 100%; min-height: 44px;
    display: inline-flex; align-items: center; justify-content: center;
  }
}

/* A control, on its own line, rather than a package name inside a sentence. */
/* Its own line, not trailing the status pill: inline beside the pill it read as
 * a caption on the pill rather than somewhere to go. No underline until hover,
 * which is how every other link in the app behaves. */
.package-governed-link { display: block; margin-top: 10px; font-size: 13px; color: var(--rust); }


/* Packages on a phone (§2.6) ------------------------------------------------
 *
 * The table is already cards at this width, so a package is a card too: the
 * caret down the left, name and counts stacked, status and cost down the right.
 * 56px minimum on the header row and on every nested row, because both are
 * tappable. */
@media (max-width: 900px) {
  .package-row {
    grid-template-columns: 22px minmax(0, 1fr) auto;
    grid-template-rows: auto auto;
    gap: 2px 8px;
    min-height: 56px;
    padding: 10px 14px;
  }
  .package-row .col-nec,
  .package-row .col-room,
  .package-row .col-type,
  .package-row .col-owner { display: none; }
  .package-row .col-check { grid-area: 1 / 1 / 3 / 2; align-self: stretch; }
  /* 22px of column on a phone, so the target reaches left into the card's own
   * padding rather than rightwards over the name. */
  .package-row .col-check::after { inset: -2px -8px -2px -14px; }
  .package-row .col-title { grid-area: 1 / 2 / 3 / 3; }
  .package-row .col-status { grid-area: 1 / 3 / 2 / 4; justify-self: end; }
  .package-row .col-cost { grid-area: 2 / 3 / 3 / 4; justify-self: end; text-align: right; }

  .package-block { border-radius: var(--r-xl); }
  /* The indent is smaller than the desktop's 28px: a phone card has less width
   * to give away, and the card edge already says these belong to the block. */
  .punch-row.is-packaged .col-title { padding-left: 14px; }
  .punch-row.is-packaged { min-height: 56px; }

  .package-callout { flex-direction: column; align-items: stretch; gap: 8px; }
  .package-callout .package-requote { min-height: 44px; }
}

@media (max-width: 640px) {
  /* The naming dialog becomes a sheet, like every other dialog on a phone. */
  .package-dialog {
    top: auto;
    bottom: 0;
    left: 0;
    transform: none;
    width: 100%;
    max-width: none;
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    padding: 16px 16px calc(16px + env(safe-area-inset-bottom, 0px));
  }
  .package-dialog .actions button { min-height: 44px; }
  .package-item-remove, .package-copy { min-height: 44px; display: inline-flex; align-items: center; }
}

/* The circle beside each punch item is the only place necessity appears, so
   the legend is what makes it readable rather than decorative. */

/* --- Walkthrough import -------------------------------------------------- */

/* .page-narrow is gone; use `class="page narrow"`. */

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

.callout-warn {
  background: var(--surface-tint); border: 1px solid var(--border-strong);
  border-radius: var(--r-xl); padding: 16px 20px; margin-bottom: 20px;
  font-size: 13px; color: var(--text-secondary);
}

.import-intro {
  margin: 0 0 20px; font-size: 13px; line-height: 1.6;
  color: var(--text-secondary);
}

.import-limits { margin: -8px 0 16px; font-size: 12px; color: var(--text-faint); }

.import-error {
  margin: 0 0 12px; font-size: 13px; color: var(--rust);
  background: #EAD8D1; border-radius: var(--r-md); padding: 9px 12px;
}

.import-actions { display: flex; gap: 12px; align-items: center; }
.import-actions .button-primary, .import-actions .button-ghost, .import-actions .button-outline { width: auto; margin: 0; }

.import-progress-line {
  display: flex; align-items: center; gap: 10px;
  margin: 0 0 8px; font-size: 14px; font-weight: 500;
}

.spinner {
  width: 15px; height: 15px; border-radius: 50%; flex: none;
  border: 2px solid var(--border-strong); border-top-color: var(--rust);
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 3s; } }

/* The reconciliation panel. */
.import-summary { margin-bottom: 24px; }
.import-stats { display: flex; gap: 40px; margin-bottom: 16px; flex-wrap: wrap; }
.import-stats > div { display: flex; flex-direction: column; gap: 2px; }
.import-stat { font: 600 30px/1 Rubik, sans-serif; color: var(--ink); }

.import-accounting {
  margin: 0; font-size: 13px; padding: 9px 12px; border-radius: var(--r-md);
}
.import-accounting.is-ok { background: #DDE3D6; color: #3E4A3C; }
.import-accounting.is-bad { background: #EAD8D1; color: var(--rust); font-weight: 500; }

/* A NOT READ count that is not zero is the one number on this screen that
   means the import is incomplete, so it does not look like the others. */
.import-stats > div.is-lost .import-stat { color: var(--rust); }
.import-stats > div.is-lost .label { color: var(--rust); }

/* The lines the parser could not place, named rather than only counted. */
.import-dropped {
  margin: 0 0 16px;
  padding: 10px 12px;
  background: #EAD8D1;
  border-radius: var(--r-md);
}
.import-dropped > summary { font-size: 12.5px; font-weight: 500; color: var(--rust); cursor: pointer; }
.import-dropped ul { margin: 8px 0 0; padding-left: 0; list-style: none; }
.import-dropped li { display: grid; gap: 2px; padding: 6px 0; border-top: 1px solid #DCC6BD; }
.dropped-line { font-size: 11px; letter-spacing: 0.5px; color: var(--rust); }
.dropped-text { font-size: 13px; color: var(--text); }
.dropped-why { font-size: 12px; color: var(--text-muted); }
.import-summary .subline { margin-top: 10px; }

.import-group { margin-bottom: 32px; }
.import-group h2 {
  font: 400 22px/1.2 'Libre Baskerville', serif; margin: 0 0 4px;
  display: flex; align-items: baseline; gap: 10px;
}
.import-count {
  font: 600 11px/1 Rubik, sans-serif; letter-spacing: 1px;
  background: var(--surface-tint); color: var(--text-secondary);
  padding: 5px 10px; border-radius: var(--r-pill);
}
.import-group > .subline { margin-bottom: 14px; }

.import-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }

.import-row {
  display: flex; gap: 14px; padding: 16px 18px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-xl);
}
.import-row.is-off { background: var(--surface-raised); border-style: dashed; }
.import-row.is-off .import-body > :not(.import-source):not(.import-flag) { opacity: 0.55; }
.import-row.is-flagged { border-left: 3px solid var(--rust); }
.import-row.is-empty { border-color: #D9B3A5; }

.import-tick { flex: none; padding-top: 2px; }
.import-tick input { width: 18px; height: 18px; accent-color: var(--forest); cursor: pointer; }

.import-body { flex: 1; min-width: 0; display: grid; gap: 8px; }

.import-flag {
  margin: 0; font-size: 12px; line-height: 1.5; color: #6B4A23;
  background: #EDDFC3; padding: 7px 10px; border-radius: var(--r-md);
}
.import-flag-quiet { background: var(--surface-tint); color: var(--text-secondary); }

.import-title {
  font: 500 14px Rubik, sans-serif; color: var(--text);
  padding: 7px 10px; border: 1px solid var(--border-strong);
  border-radius: var(--r-md); background: var(--surface-raised); width: 100%;
}
.import-desc {
  font: 400 13px/1.5 Rubik, sans-serif; color: var(--text-secondary);
  padding: 7px 10px; border: 1px solid var(--border-strong);
  border-radius: var(--r-md); background: var(--surface-raised);
  width: 100%; resize: vertical; min-height: 38px;
}

.import-meta { display: flex; gap: 8px; flex-wrap: wrap; }
.import-meta select, .import-room {
  font: 400 12px Rubik, sans-serif; color: var(--text-secondary);
  padding: 6px 9px; border: 1px solid var(--border-strong);
  border-radius: var(--r-md); background: var(--surface-raised);
}
.import-meta select { text-transform: capitalize; }
.import-room { flex: 1; min-width: 130px; }

/* The source lines. Quiet, monospaced, and never hidden. */
.import-source {
  list-style: none; margin: 2px 0 0; padding: 10px 12px;
  background: var(--surface-tint); border-radius: var(--r-md);
  display: grid; gap: 4px;
}
.import-source li { display: flex; gap: 10px; font-size: 12px; line-height: 1.5; }
.import-source li.is-note { padding-left: 16px; }
.import-lineno {
  flex: none; width: 26px; text-align: right;
  font: 400 11px/1.6 ui-monospace, Menlo, monospace; color: var(--text-faint);
}
.import-linetext {
  font: 400 12px/1.5 ui-monospace, Menlo, monospace; color: var(--text-secondary);
  white-space: pre-wrap; word-break: break-word;
}

/* Sticky, because the decision is made while scrolling seventy rows. */
.import-bar {
  position: sticky; bottom: 0; z-index: 5;
  display: flex; align-items: center; gap: 20px; flex-wrap: wrap;
  padding: 14px 20px; margin-top: 8px;
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--r-xl); box-shadow: 0 -6px 20px rgba(36, 27, 18, 0.07);
}
.import-bar-count { margin: 0; font-size: 13px; color: var(--text-secondary); }
.import-bar-count strong { font: 600 18px Rubik, sans-serif; color: var(--ink); }
.import-bar .import-error { margin: 0; flex: 1; }
.import-bar-actions { margin-left: auto; display: flex; gap: 10px; }
.import-bar-actions .button-primary, .import-bar-actions .button-ghost, .import-bar-actions .button-outline {
  width: auto; margin: 0;
}
.import-bar-actions .button-primary[disabled] { opacity: 0.5; cursor: default; }

@media (max-width: 640px) {
  .import-stats { gap: 24px; }
  .import-bar-actions { margin-left: 0; width: 100%; }
}

.import-source-picker {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-bottom: 16px;
}
.import-file { width: auto; margin: 0; cursor: pointer; }
.import-or { font-size: 12px; color: var(--text-faint); }
.import-filename { font-size: 12px; color: var(--text-secondary); }
.import-note {
  margin: -8px 0 14px; font-size: 12px; color: #3E4A3C;
  background: #DDE3D6; border-radius: var(--r-md); padding: 8px 11px;
}

/* The Saving… state — ui-audit-spec §3. Same fill, 70% opacity, so the button
 * reads as busy rather than as a different button. */
.button-primary.is-saving,
.button-primary.is-saving:disabled {
  background: var(--forest);
  color: #F3EEE0;
  opacity: 0.7;
  cursor: default;
}

/* Ad-hoc checklist items on a step — post-it notes.
 *
 * Quieter than the template checklist above them: these are somebody's
 * reminder, not part of the launch's shape. Never shown to an owner, never
 * counted in a percentage. */
.step-notes { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--border-soft); }
.step-notes .label { margin-bottom: 8px; }
.step-notes-empty { margin-bottom: 10px; }

.step-note-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 0;
  min-height: 32px;
}
.step-note-row + .step-note-row { border-top: 1px solid var(--border-soft); }
.step-note-text { flex: 1; min-width: 0; font-size: 13px; color: var(--text); }
.step-note-row .ticked ~ .step-note-text {
  color: #9A8F79;
  text-decoration: line-through;
  text-decoration-color: #C9BCA0;
}
.step-note-row .icon-button { opacity: 0; transition: opacity .12s; }
.step-note-row:hover .icon-button,
.step-note-row:focus-within .icon-button { opacity: 1; }
.step-notes .add-row { margin-top: 10px; }

/* Column sort controls. The head is 10px letterspaced caps, so the control has
 * to inherit that rather than look like a link dropped into it. */
.col-sort {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
}
.col-sort:hover { color: var(--rust); text-decoration: none; }
.col-sort.is-sorted { color: var(--text); }
.col-caret { font-size: 9px; line-height: 1; }
.col-nec .col-sort { font-size: 11px; letter-spacing: 0; }
.sort-state { color: var(--text-muted); }

/* Header at phone width — mobile-spec §1 ------------------------------------
 *
 * Placed at the end of the sheet deliberately: every rule it overrides
 * (.lockup, .appbar-nav, .nav-item, .signout) is defined near the top, and a
 * media block that sits *above* the rule it means to override loses on source
 * order and computes silently wrong. That has happened twice in this file.
 *
 * Measured at 393pt, with the lockup frozen so nothing could shrink out of
 * sight: the row needs 345px against 361px of content box. 16px of slack.
 *
 * Three changes do that work. The spec lists a fourth — drop the username —
 * which saves nothing, because .appbar-username already carries .hide-narrow
 * and is display:none below 900px. It was done before this batch began. */
@media (max-width: 640px) {
  /* Scaled lockup: 130px -> 114px. The spec's table asks for this and its
   * numbered list omits it; it is the single largest saving of the three. */
  .lockup-name { font-size: 14px; }
  .lockup-org { font-size: 8px; letter-spacing: 1.5px; }

  /* The 28px margin + 28px padding + rule is a desktop separation. At 393pt it
   * is 45px spent on whitespace between two things already distinguishable. */
  .appbar-nav {
    margin-left: 16px;
    padding-left: 0;
    border-left: none;
  }

  /* 7px 14px -> 8px 12px. Height comes from the bar's 56px, not the pill's own
   * padding, so the tap band is the full bar height either way. */
  .nav-item { padding: 8px 12px; }

  /* It wrapped to two lines at 28px wide, which is what made the bar 77px look
   * like a wrapping problem rather than a padding one. */
  .signout { white-space: nowrap; }
}

/* Native date inputs at phone width — mobile-spec §2 -------------------------
 *
 * Kept native: this is containment, not replacement.
 *
 * Measured first, and the reported overflow does not reproduce in a Chromium
 * viewport at 393pt. All thirteen date inputs — two on new launch, four on
 * settings, one in the punch drawer, six in the step panels — render exactly to
 * their container and 44px tall. The base `input[type="date"]` rule already
 * carries `width: 100%`, `* { box-sizing: border-box }` is global, and the one
 * flex parent (.field-row .field) already carries `min-width: 0`.
 *
 * The fault is iOS Safari's, which sizes a date input from its intrinsic
 * content and can ignore a width it has been given. `-webkit-appearance: none`
 * is the fix for that, and it is scoped to ≤640px on purpose: at desktop width
 * it would strip Chrome's calendar-picker indicator from a control that has
 * room for it and is being used with a pointer.
 *
 * The rest is a safety net. It costs nothing where the layout is already right
 * and holds the line if a future two-up grid puts one of these in a flex or
 * grid cell — which mobile-spec §6 will, for Cost Low / Cost High. */
@media (max-width: 640px) {
  input[type="date"] {
    -webkit-appearance: none;
    appearance: none;
    max-width: 100%;
    min-width: 0;          /* flex and grid children default to min-width: auto */
    min-height: 44px;
  }

  /* A date input in a flex or grid cell overflows unless the cell may shrink
   * below its content. `1fr` alone will not do it; `minmax(0, 1fr)` will. */
  .field-row > .field,
  .grid-fields > .field { min-width: 0; }
}

/* Tap targets at phone width — mobile-spec cross-cutting --------------------
 *
 * Audited at 393pt by measuring every interactive element's hit box, not by
 * reading the stylesheet. The rules below fix the four that matter.
 *
 * The list of remaining misses was re-measured at 393pt in batch 3, and two of
 * the five were never or are no longer failures:
 *
 *   - Portfolio row title: not a failure, and never was. The link's own box is
 *     97x22, but its ::after is absolute at inset 0 against the card, so the
 *     target is the whole 361x123 row — five probe points across it all land on
 *     the link. The original finding measured the box and not the target.
 *   - Drawer room select: fixed in batch 2 by `[data-drawer] select`. Measured
 *     360x44.
 *   - Supply rail steps: fixed below.
 *
 * The last two were fixed in batch 3, at the 900px block where each one's
 * mobile layout is already defined:
 *
 *   - Punch row title (a.col-title): measured 239x21 on a one-line title,
 *     239x40 on two. Worth keeping the reason, because the source looks
 *     identical to the portfolio card and behaves the opposite way: both put an
 *     ::after at inset 0, but .col-title is positioned and therefore its own
 *     containing block, so the pseudo stretches over the title cell rather than
 *     the row. Same construction, opposite outcome — the portfolio link's
 *     target is the whole card, this one's was the cell.
 *   - Filters summary: 323x40, and `display: block` under a min-height would
 *     have left the label at the top of the box.
 *
 * Segmented controls are deliberately exempt from the 8px separation rule.
 * The nav pills, Active/Archive and Steps/Punch list sit 4px apart because
 * they are one control, and a mis-tap inside a segmented control costs a tap.
 * The rule exists for unrelated controls with different consequences — which
 * is exactly the substep edit/delete pair below.
 *
 * The punch row checkboxes are not here. They already draw a 26px box inside a
 * 44px hit area, which is the pattern; nudging them to 24px to match a number
 * would be churn. */
@media (max-width: 640px) {
  /* 1. The substep edit and delete icons.
   *
   * Measured 26x33 and 24x33 with a 2px gap, and the × deletes. Two targets
   * under size, close enough to catch with one thumb, one of them destructive.
   * This is the only real violation of the 8px rule on the screen. */
  .sub-actions { gap: 8px; }
  .sub-actions .icon-button {
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 15px;      /* the glyph, not the target */
  }

  /* 4. The substep tickbox: measured 24x24.
   *
   * The negative margin is the point. The button's border box is 44px and its
   * margin box is 24px, so the row lays out exactly as it did while the hit
   * area overflows symmetrically — the drawn box does not move and the text
   * does not shift. Grown any other way, a 44px control puts a heavy square
   * against every line of a nine-item checklist.
   *
   * Same construction as the punch row: the box is drawn by ::before and the
   * tick by ::after, so `font-size: 0` can hide the ✓ that lives in the
   * button's text content without hiding the mark. aria-label carries the
   * name either way. */
  .sub .tickbox {
    position: relative;
    width: 44px;
    height: 44px;
    margin: -10px;
    padding: 0;
    border: none;
    background: transparent;
    font-size: 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .sub .tickbox::before {
    content: '';
    width: 24px;
    height: 24px;
    border: 1.5px solid #C9BCA0;
    border-radius: 7px;
    background: var(--surface);
  }
  .sub .tickbox.ticked::before { background: var(--forest); border-color: var(--forest); }
  .sub .tickbox.ticked::after {
    content: '✓';
    position: absolute;
    color: #F3EEE0;
    font-size: 14px;
    line-height: 1;
  }
  .sub .tickbox:disabled { cursor: default; }
  .sub .tickbox:disabled::before { opacity: .5; }

  /* 2 and 3. The header: lockup 114x28, nav pills 79x36 and 60x36, Sign out
   * 48x15 — all measured, all short.
   *
   * mobile-spec §1 claims the pill "stays ≥ 44px via the row's vertical
   * padding — the bar's full 56px height is the tap band". Hit-testing down
   * the bar's centre line says otherwise: the pill answers from y=10 to y=44,
   * a 36px band. The bar's padding is not clickable. Height has to be on the
   * controls themselves.
   *
   * 44px inside a 56px bar leaves 6px above and below, so none of this changes
   * the bar's height or the 16px of horizontal slack. */
  .lockup { min-height: 44px; justify-content: center; }
  .nav-item {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
  .signout {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  /* The supply wizard's rail steps: measured 34x(80..159) at 393pt.
   *
   * Height only — every step is already wide enough, the shortest being "2
   * Beds" at 80px. The button is display:flex with align-items:center, so
   * min-height centres the label rather than stranding it at the top.
   *
   * The 6px gap stays. The rail is one control that steps through one wizard,
   * so it takes the same exemption as the nav pills and Steps/Punch list: a
   * mis-tap inside it costs a tap, not a wrong destructive action. */
  .rail-step { min-height: 44px; }
}

/* Portfolio cards at phone width — mobile-spec §3 ---------------------------
 *
 * Restored from the Portfolio mobile artboard: name + type chip, full-width
 * bar, meta line, flag pills last. Settings is added to the meta line, which
 * the artboard does not have.
 *
 * Flags last is the point of the reorder. They used to interrupt the middle of
 * the card, which is what made two cards of different heights read as two
 * different layouts. At the end, a launch with nothing flagged simply stops
 * earlier — the rhythm above it is identical.
 *
 * The order is done with grid areas rather than by moving the markup, so the
 * desktop row is untouched. `display: contents` on the two wrappers and on
 * .row-progress is what lets their children be placed: grid only positions its
 * own children, and title, bar, %, meta and Settings live three levels apart.
 *
 * Not changed, because it was already right: the whole card navigates. A
 * stretched ::after on the title link covers the row and Settings sits above it
 * at z-index 2. Hit-tested across a 5x5 grid of points at 393pt — every point
 * answers with the card's link, and a tap on Settings answers with Settings. */
@media (max-width: 640px) {
  /* The list stops being one bordered card and becomes a stack of them. */
  .card.list {
    padding: 0;
    background: none;
    border: none;
    border-radius: 0;
    overflow: visible;
  }

  .launch-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "title    pct"
      "track    track"
      "meta     settings"
      "dates    dates"
      "flags    flags";
    align-items: center;
    column-gap: 10px;
    /* No row-gap. The dates and flags areas are empty on most launches, and a
     * gap applies between rows whether or not they hold anything — 18px of
     * dead space at the foot of every card without chips. Spacing goes on the
     * elements instead, so an absent one contributes nothing at all. */
    row-gap: 0;
    padding: 14px;
    margin: 0 0 10px;
    border: 1px solid var(--border);
    border-radius: var(--r-xl);
  }
  .launch-row:last-child { margin-bottom: 0; }

  /* So the grid can reach past the wrappers to the parts themselves. */
  .launch-main,
  .launch-side,
  .row-progress { display: contents; }

  .launch-title { grid-area: title; }
  .pct {
    grid-area: pct;
    justify-self: end;
    /* Aligned to the first line of the title, not the middle of it. A long
     * name wraps and the chip drops to a second line; centred, the percentage
     * floated between the two. */
    align-self: start;
    padding-top: 2px;
    min-width: 0;
    font-size: 15px;
    color: var(--forest);
  }
  .track {
    grid-area: track;
    width: 100%;
    height: 6px;
    border-radius: 3px;
    margin: 9px 0;
  }
  .row-meta { grid-area: meta; font-size: 11.5px; }
  /* Settings keeps the 44px it gained in batch 1. mobile-spec §3 says "min
   * 32px tap height" here, which contradicts the same document's cross-cutting
   * 44px minimum; the larger of the two wins. */
  .row-link { grid-area: settings; justify-self: end; }
  .row-dates { grid-area: dates; margin-top: 9px; }
  .row-flags { grid-area: flags; margin-top: 9px; }

  /* "New launch" returns to a compact pill beside the heading rather than a
   * full-width block above the tabs — artboard again. The ≤900px rule that
   * stacks .page-head is what made it full width. */
  /* Row rather than the ≤900px stack, so "New launch" can sit beside the
   * Portfolio heading as a pill rather than spanning the page as a block.
   *
   * Wrapping, because this rule reaches every page-head and they do not all
   * carry one short control: the punch list's holds Export for owner *and*
   * Settings, which squeezed the Export button to 81px and broke its label
   * across three lines. Where the actions fit beside the heading they stay
   * there; where they do not they take their own line, as they used to. */
  .page-head { flex-direction: row; align-items: center; gap: 12px; flex-wrap: wrap; }
  .page-head > div { min-width: 0; }
  /* Never shrink below the label: wrapping is the intended overflow here. */
  .page-head > .head-actions { flex: none; }
  .page-head .button-primary.inline,
  .page-head a.button-primary {
    width: auto;
    margin-top: 0;
    flex: none;
    min-height: 44px;
    padding: 9px 14px;
    font-size: 12.5px;
    display: inline-flex;
    align-items: center;
  }
}

/* Anchor-styled buttons centre their label at phone width ------------------
 *
 * Specificity, and then source order. `a.button-ghost { display: inline-block }`
 * is declared globally at the top of this file with specificity (0,1,1); the
 * ≤900px rule that gives these buttons their 44px reads `.button-ghost` at
 * (0,1,0) and sits above it. So the anchor variants stayed inline-block, the
 * min-height grew the box, and the label stayed at the top of it — Settings on
 * a Portfolio card sat 14px above the meta line it shares, reading as its own
 * row rather than as part of that line.
 *
 * Matching (0,1,1) and placed last is what makes this win. The <button>
 * variants are already handled by the ≤900px rule; only the anchors need it. */
@media (max-width: 900px) {
  a.button-ghost, a.button-outline, a.button-primary,
  a.button-danger, a.button-danger-ghost,
  /* `.button-primary.inline` is (0,2,0) and also declares inline-block, so the
   * anchor selectors above lose to it at (0,1,1). Matching it with an element
   * qualifier takes it to (0,2,1). Found by measuring, not by reading: "Build
   * the supply list" was the last label still sitting 3px high. */
  a.button-primary.inline, a.button-outline.inline, a.button-ghost.inline {
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
}

/* The progress card's footnote — mobile-spec §4.
 *
 * Signed date and leads, moved out of the page header where they were the
 * third and fourth facts on a line that wrapped at 393pt. Sits under the phase
 * rail, separated by the card's own divider weight. */
.progress-footnote {
  margin: 16px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--border-soft);
  font-size: 11.5px;
  color: var(--text-muted);
}

/* The step's four status chips, at phone width — mobile-spec §4.
 *
 * Measured at 393pt: they wrap 3+1 — "Not started, In progress, Complete" on
 * one row and "N/A" alone on the next, which reads as an afterthought rather
 * than as the fourth of four equals.
 *
 * A 2x2 grid instead of wrapping flex. minmax(0, 1fr) rather than 1fr so a
 * long label shrinks its cell instead of pushing the grid wider than the card.
 *
 * Scoped to .status-row: the offer-flow chip groups next to it hold two and
 * three chips, and a 2x2 would leave those with a hole in the corner. */
@media (max-width: 640px) {
  .status-row .chips {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
  }
  .status-row .chips .chip-button {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-align: center;
  }
}

/* Punch list at phone width — mobile-spec §5 --------------------------------
 *
 * The necessity dot, restored from the artboard: inline before the title,
 * baseline-aligned, 8px gap, with the meta line indented to clear it. It was
 * `display: none` on mobile while the legend above the list went on explaining
 * what the dots mean — a key to something not on screen.
 *
 * The artboard's 21px meta indent is drawn at 430pt. Rather than hardcode it,
 * the dot gets its own grid column and the meta lines start in the title's
 * column, so the indent is the dot's width plus the gap at whatever width the
 * screen happens to be. */
@media (max-width: 640px) {
  .punch-row {
    grid-template-columns: 44px auto minmax(0, 1fr);
    grid-template-areas:
      "check nec title"
      "check .   meta"
      "check .   pills";
  }
  .punch-row .col-nec {
    display: block;
    grid-area: nec;
    align-self: baseline;
    text-align: left;
  }
  .punch-row .col-nec .nec-dot { font-size: 13px; line-height: 1; }
  .punch-row .col-title { align-self: baseline; }

  /* Six figures as 3x2 rather than four across and the rest orphaned below,
   * which left the right half of the second row empty. */
  .stat-row {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 16px 12px;
  }
  .stat-row > :last-child { grid-column: 1 / -1; }

  /* Clear beside Apply. Both were full-width blocks stacked in the panel's
   * column, where `flex: 1` sizes height rather than width. */
  .filter-actions {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    gap: 8px;
    width: 100%;
  }
  .filter-actions > * { width: auto; min-width: 0; min-height: 44px; }
}

/* The mobile sort control — mobile-spec §5.
 *
 * Hidden rather than absent above 640px. Desktop sorts by clicking a column
 * head, but these fields still carry the current sort through a filter Apply —
 * which is what the hidden inputs they replaced were for. A display:none select
 * still submits its value, so one pair of fields serves both surfaces. */
.sort-row { display: none; }
.sort-field { display: block; min-width: 0; }
.sort-field .label { display: block; margin-bottom: 4px; }
.sort-field select { width: 100%; min-height: 44px; }

@media (max-width: 640px) {
  .sort-row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
    width: 100%;
  }
}

/* Punch item drawer at phone width — mobile-spec §6 -------------------------
 *
 * The title is the primary field on this screen and read at arm's length in a
 * house; it was rendering at the same 14px as every secondary input.
 *
 * Kept as an <input> rather than the textarea §6 asks for. The save path reads
 * `field.value`, so either works, but a textarea accepts newlines in a field
 * that is a one-line title everywhere it is displayed — the row, the export,
 * the drawer header. The size was the stated problem and the size is fixed. */
@media (max-width: 640px) {
  [data-drawer] [data-punch-field="title"] {
    font-size: 17px;
    min-height: 52px;
  }

  /* Measured 105x44 with padding: 0 — the label had no padding of its own and
   * the button inside it supplied none either. */
  .photo-add {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    padding: 0 14px;
    border: 1px solid var(--border-strong);
    border-radius: var(--r-md);
    font-size: 12.5px;
    font-weight: 500;
    color: var(--rust);
  }

  /* Cost low and high, side by side. The generic .field-row stacks at this
   * width for the date pairs, which need the room; this pair does not. */
  .field-row-pair {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px;
  }
  .field-row-pair > .field { min-width: 0; margin-bottom: 0; }

  /* Deferred from batch 1: measured 360x36. */
  [data-drawer] select { min-height: 44px; }
}

/* The crumb bar's section mode — mobile-spec §7.
 *
 * Same bar, same height, same rule: only the contents swap. The name takes the
 * page's serif so it reads as a heading rather than as another crumb, and the
 * count sits right so the two ends of the bar carry the two facts. */
.crumb-section {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  min-width: 0;
}
.crumb-section-name {
  font-family: 'Libre Baskerville', Georgia, serif;
  font-size: 15px;
  color: var(--rust);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.crumb-section-count {
  flex: none;
  font-size: 11px;
  color: var(--text-muted);
}

/* Comparing quotes and awarding (§3) -----------------------------------------
 *
 * One set of markup at both widths. The totals come first in the document
 * because that is the comparison someone opened this to make (§3.3 puts the
 * card on top; a desktop reads it the same way), and the grid below it is a
 * real table — items down, vendors across — that becomes one card per item on
 * a phone by changing what the cells display, not by rendering twice. */

.compare-caption { margin: 0 0 16px; font-size: 12px; color: var(--text-secondary); }
.compare-empty { margin: 0; font-size: 13px; color: var(--text-secondary); }

.compare-totals { list-style: none; margin: 0 0 24px; padding: 0; }
.compare-total-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto auto;
  align-items: center;
  gap: 12px;
  min-height: 56px;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  margin-bottom: 8px;
  background: var(--surface);
}
.compare-total-row.is-awarded { background: #DDE3D6; border-color: #C7D2BE; }
/* Visible and out of the running: §3.1's 45%, which keeps the figure legible
 * as a record of what was withdrawn without letting it read as an offer. */
.compare-total-row.is-void { opacity: 0.45; }

.compare-vendor { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.compare-vendor-name { font-size: 13px; font-weight: 600; color: var(--ink); }
.compare-mode { font-size: 10px; letter-spacing: 1.5px; color: var(--text-faint); }
.compare-sub { font-size: 11px; color: var(--text-secondary); }

.compare-figure { display: flex; align-items: baseline; gap: 8px; white-space: nowrap; }
.compare-total { font-size: 15px; font-weight: 600; color: var(--ink); }
.compare-lowest { font-size: 11px; color: #5C6B58; }
.compare-waiting { font-size: 12px; color: var(--text-faint); }
.compare-expires { font-size: 11px; color: var(--text-secondary); }
.compare-state { display: flex; align-items: center; gap: 8px; }
.compare-void-pill { background: #EAD8D1; color: var(--rust); }
.is-struck { text-decoration: line-through; }

/* The grid. The item column stays put when the vendors overflow it, because
 * scrolling sideways past the name of the thing being priced is how a price
 * ends up read against the wrong row. */
.compare-grid-wrap { overflow-x: auto; }
.compare-grid { width: 100%; border-collapse: collapse; }
.compare-grid th, .compare-grid td { padding: 10px 12px; text-align: left; vertical-align: top; }
.compare-grid thead th {
  border-bottom: 1px solid var(--border-strong);
  font-weight: 400;
}
.compare-grid thead th > * { display: block; }
.compare-grid tbody tr + tr th, .compare-grid tbody tr + tr td {
  border-top: 1px solid var(--border-soft);
}
.compare-col { min-width: 160px; }
.compare-col.is-awarded { background: #DDE3D6; }
.compare-col.is-void, .compare-cell.is-void { opacity: 0.45; }
/* Pinned, and visibly so.
 *
 * Scrolling sideways slides a vendor's column underneath this one, and with no
 * edge to it the half-covered column read as a broken one: a fragment of a
 * name and part of a pill, squeezed against the item. The border and the
 * shadow say the column is on top and the rest continues beneath it. */
.compare-item-head, .compare-item {
  position: sticky; left: 0; z-index: 2;
  background: var(--surface);
  min-width: 180px;
  font-weight: 400;
  box-shadow: 1px 0 0 var(--border-strong), 6px 0 8px -6px rgba(36, 27, 18, 0.18);
}
.compare-item-title { display: block; font-size: 13px; font-weight: 500; color: var(--ink); }
.compare-item-room { display: block; font-size: 11.5px; color: var(--text-secondary); }
.compare-cell { text-align: right; }
.compare-price { font-size: 13px; color: var(--ink); }
/* The lowest in the row, among the quotes that are actually in the row. A lump
 * sum is not one of them — see src/engine/quoteComparison.js. */
.compare-price.is-lowest { font-weight: 600; color: var(--rust); }
.compare-none { color: var(--text-faint); }
.compare-in-lump { font-size: 12px; font-style: italic; color: var(--text-secondary); }
.compare-in-lump-short { display: none; }
.compare-cell.is-lump { background: var(--surface-raised); text-align: center; }

/* A lump sum's price belongs in its column.
 *
 * §3.1 asks for one merged cell saying "One total — not itemized", and a table
 * cannot merge cells by CSS, so the first attempt put the words in the top row
 * and left the rest tinted and empty — which read as a vendor who priced the
 * first item and skipped the other four. Now the figure is in the column
 * header, where it is unmissable, and every row says the item is covered by
 * it, in §4.4's word: `Included` never reads as a missing price. */
.compare-col-total { font-size: 13px; font-weight: 600; color: var(--ink); }

/* The estimate link, as a thing to hand over rather than debug output.
 *
 * It used to render as a bare path beside a text link — which read as a
 * fragment of something, and gave a staff member whose clipboard permission is
 * blocked no way at all to get the URL out. A read-only input can be selected
 * and copied by hand, and holds the whole address. */
.compare-send { margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border); }
.compare-sent { margin: 0 0 10px; font-size: 12px; color: var(--text-secondary); }
.compare-link-label { display: block; margin-bottom: 6px; }
.compare-link-row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
.compare-link-input {
  flex: 1 1 auto; min-width: 0; height: 36px; padding: 0 10px;
  background: var(--surface-raised); border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  font-family: inherit; font-size: 12.5px; color: var(--text-secondary);
  text-overflow: ellipsis;
}
.compare-link-input:focus { outline: 2px solid var(--rust); outline-offset: 1px; }
/* The copy confirmation is a state, not only a word: the tick and the colour
 * are what someone sees from across the desk. */
.compare-link-row [data-copy-link].is-copied { border-color: var(--forest); color: var(--forest); }
.compare-send-note { margin: 0 0 10px; font-size: 12px; color: var(--text-secondary); }

.compare-notes { margin-top: 20px; }
.compare-note { margin: 0 0 8px; font-size: 12px; color: var(--text-secondary); }
.compare-note.is-void { opacity: 0.45; }
.compare-note-who { font-weight: 600; margin-right: 6px; }

/* Desktop keeps every column: one you ignore costs a glance. The line naming
 * the columns a phone leaves out is therefore for the phone only. */
.compare-aside { display: none; }

/* Phone (§3.3): the totals card stays a list of rows, and the table becomes
 * one card per item — the same cells, labelled with the vendor they belong to
 * rather than placed under a column heading that has scrolled away. */
@media (max-width: 640px) {
  /* The breakdown exists to compare per-item prices, and a vendor without any
   * is five rows to scroll past rather than one column to ignore: five
   * vendors over five items was thirty lines carrying ten prices. Hidden by
   * width from the same markup, so the two views cannot drift apart.
   *
   * A comparable column with no line for one item is NOT hidden — its em dash
   * is a gap in an itemised quote, which is worth reading. See
   * src/engine/quoteComparison.js. */
  .compare-cell.is-aside { display: none; }
  .compare-aside { display: block; margin: 0 0 12px; font-size: 12px; color: var(--text-secondary); }
  /* Nothing itemised at all: cards of titles with no prices under them say
   * less than one sentence does. */
  .compare-grid-wrap.is-aside-only { display: none; }

  .compare-total-row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 6px; }
  .compare-total-row .compare-state { grid-column: 1; }
  .compare-total-row .compare-act { grid-column: 2; justify-self: end; }

  .compare-grid, .compare-grid tbody, .compare-grid tr, .compare-grid th, .compare-grid td {
    display: block;
  }
  .compare-grid thead { display: none; }
  .compare-grid tbody tr {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    margin-bottom: 12px;
    padding: 12px;
  }
  .compare-grid tbody tr + tr th, .compare-grid tbody tr + tr td { border-top: none; }
  .compare-item { position: static; padding: 0 0 8px; min-width: 0; }
  .compare-cell {
    display: flex; justify-content: space-between; gap: 12px;
    padding: 4px 0; text-align: left; background: none;
  }
  /* The vendor's name in front of their figure, because the column header is
   * not on screen to say whose it is. */
  .compare-cell::before {
    content: attr(data-compare-name);
    font-size: 13px; color: var(--text-secondary);
  }
  /* Every card names the lump sum, and in §3.3's shorter wording: here the
   * line sits beside one item, not at the head of a column. */
  .compare-grid tbody tr .compare-in-lump { display: none; }
  .compare-in-lump-short { display: inline; font-size: 12px; font-style: italic; color: var(--text-secondary); }
}

/* The package drawer on a phone (§2.1 Mobile, §2.8, §2.9, §3.3) -------------
 *
 * The drawer already went full screen on a phone with a sticky head, from the
 * item drawer it shares that CSS with. What it did not have was a shape: it
 * was the desktop bar — a 20px serif name, a 32px pencil beside it, a × at the
 * end — stretched across a screen it now owned. §2.1 gives it the shape a
 * full-screen view is expected to have, and the two mobile-only controls that
 * shape needs are hidden everywhere else. */
.drawer-back, .drawer-menu, .drawer-action-bar { display: none; }

/* §3.3: the way into the drawer from the bottom of an expanded card. Shown at
 * every width — the card's only other way in is the name in its head, which
 * is above everything you scrolled past to get here. 44px, and the full width
 * of the card on a phone where there is nothing to sit beside. */
/* §3.3's way in belongs to the phone. On a desktop the row itself opens the
 * drawer, so this would be the same door twice — and a lone bordered button
 * in an otherwise empty row under the items reads as a layout fault. */
.package-open-foot { display: none; margin-top: 12px; }
.package-open.button-outline { min-height: 44px; }

@media (max-width: 900px) {
  /* Landscape on a notched phone puts the cutout over the left or right edge,
   * and viewport-fit=cover is what lets it. The gutter takes whichever is
   * larger rather than adding them, so portrait is unchanged. */
  .page {
    padding-left: max(var(--gutter), env(safe-area-inset-left, 0px));
    padding-right: max(var(--gutter), env(safe-area-inset-right, 0px));
  }
  .appbar-inner {
    padding-left: max(var(--gutter), env(safe-area-inset-left, 0px));
    padding-right: max(var(--gutter), env(safe-area-inset-right, 0px));
  }
  .drawer {
    padding-left: max(16px, env(safe-area-inset-left, 0px));
    padding-right: max(16px, env(safe-area-inset-right, 0px));
  }

  .drawer.package-drawer, .drawer.package-drawer.is-wide { width: 100%; }

  /* THE 56px TOP BAR. 56 of bar below the status bar, not 56 including it —
   * hence the inset on top of the min-height rather than inside it. The side
   * padding is 4px and not the body's 16px so both 44px targets can reach the
   * edges they are nearest; the name is what lines up with the body. */
  .package-drawer-head {
    min-height: calc(56px + env(safe-area-inset-top, 0px));
    gap: 0;
    padding: env(safe-area-inset-top, 0px) 4px 0;
  }
  .package-drawer-head .spacer { display: none; }
  /* The desktop shape's two controls. The pencil has no room beside a 15px
   * name and the × is the back chevron's job here, so both go and Rename
   * moves into the ⋯. */
  .package-drawer-head .package-rename,
  .package-drawer-head .drawer-close { display: none; }

  .drawer-back {
    display: inline-flex; align-items: center; justify-content: center;
    flex: none; width: 44px; height: 44px; padding: 0;
    border: none; background: none; cursor: pointer;
    font-size: 26px; line-height: 1; color: var(--text-secondary);
  }
  .drawer-back:active { color: var(--rust); }

  .package-drawer-name {
    flex: 1; min-width: 0;
    font-family: Rubik, system-ui, sans-serif;
    font-size: 15px; font-weight: 600; color: #241B12;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }

  /* The ⋯, and the menu it drops. Its own component rather than the row
   * menu's: this one hangs off a fixed bar at the top of the screen, and the
   * two have no rule in common beyond being 44px. */
  .drawer-menu { display: block; position: relative; flex: none; }
  .drawer-menu-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    width: 44px; height: 44px;
    font-size: 20px; line-height: 1; color: var(--text-secondary);
    list-style: none; cursor: pointer; border-radius: var(--r-sm);
  }
  .drawer-menu-toggle::-webkit-details-marker { display: none; }
  .drawer-menu[open] .drawer-menu-toggle { background: #F1E9D6; color: var(--rust); }
  .drawer-menu-items {
    position: absolute; top: 46px; right: 2px; z-index: 22;
    display: flex; flex-direction: column;
    min-width: 200px; padding: 6px;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-xl);
    box-shadow: 0 10px 26px rgba(36, 27, 18, 0.16);
  }
  .drawer-menu-item {
    display: flex; align-items: center;
    min-height: 44px; padding: 0 12px;
    border: none; background: none; cursor: pointer;
    font-family: Rubik, system-ui, sans-serif;
    font-size: 14px; font-weight: 500; color: var(--text);
    text-align: left; border-radius: var(--r-md);
  }
  .drawer-menu-item:active { background: #F1E9D6; }
  /* §2.8 on a phone: Delete package is in this menu, so the foot's copy of it
   * goes. It keeps being the last thing in the drawer on a desktop. */
  .drawer-menu-item.is-severe { color: var(--rust); }
  .package-drawer .package-drawer-foot { display: none; }

  /* §2.1's STICKY ACTION BAR. Fixed to the viewport, not sticky inside the
   * drawer: the drawer is the whole screen here, so the two are the same
   * place, and fixed does not depend on where in the scroll the bar's own
   * container happens to end. Above the drawer's 20, below the dialogs' 29. */
  .drawer-action-bar {
    display: block;
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 21;
    padding: 10px max(16px, env(safe-area-inset-left, 0px))
             calc(10px + env(safe-area-inset-bottom, 0px))
             max(16px, env(safe-area-inset-right, 0px));
    background: #FFFDF6;
    border-top: 1px solid #E6DCC4;
  }
  .drawer-action-bar .button-primary { width: 100%; min-height: 48px; }
  /* The outcome block's copy of the same control. One of the two is on screen
   * at any width; this is the one that goes on a phone. The sentence under it
   * is not inside it, and stays. */
  .package-drawer .outcome-action { display: none; }
  /* Room for the bar. Only where there is one — a stage that is waiting on
   * somebody else gets no bar and so needs no gap. */
  .drawer.package-drawer:has([data-sticky-action]) {
    padding-bottom: calc(78px + env(safe-area-inset-bottom, 0px));
  }

  /* §2.9: the totals rows are what the Quotes section is on a phone, so they
   * get a row's worth of height, and the control in one is a target rather
   * than a label. The full-width `Change award…` band is above, from S3. */
  .compare-total-row { min-height: 56px; }
  .compare-total-row [data-award-button] { min-height: 44px; }
}

/* A package reads as one thing (§3.1) ---------------------------------------
 *
 * Scanning the list, a package and a standalone item looked the same: a
 * slightly warmer parent row, and the words "In package" in a child's status
 * cell. The card around the block was a 1px border one step off the list's
 * own surface, and the items inside it were on the list's surface exactly, so
 * the moment your eye left the parent row there was nothing holding the group
 * together.
 *
 * NOT GREEN. #DDE3D6 means approved in this app — the approved outcome block
 * and the awarded row in the quotes grid. A package is not an outcome, and if
 * packages are green generally then green stops meaning approved.
 *
 * So the grouping is a FILL: one tint across the whole block, parent row and
 * children alike, so a package is a filled shape against the list's own
 * surface rather than a run of rows with something drawn beside them. It
 * survives both widths — a band behind table rows on a desktop, behind a
 * stack of rows on a phone — and it says containment without saying anything
 * about state. Status stays where status lives: the pill.
 *
 * A spine down the left edge was tried first and does the same job, but only
 * at the edge: the middle of a package still looked like the middle of the
 * list.
 *
 * The fill and its hover are one pair, set in one place. A hover has to be a
 * step DARKER than whatever it sits on, and the list's own #F7F0DD is lighter
 * than every candidate here — left alone it would make a hovered package row
 * appear to light up out of its own block. */
.package-block {
  --package-fill: var(--surface-tint);
  --package-hover: #EEE4CB;
  background: var(--package-fill);
  border-color: var(--border-strong);
}
.package-block .package-row:hover { background: var(--package-hover); }
/* The separators have to be darker than the fill, not lighter: --border-soft
 * is a hairline for white rows and disappears into a tint. */
.package-block[open] > .package-row,
.package-items > .punch-row { border-color: var(--border-strong); }
/* The head is part of the band, not a lid on it: a hairline, so the parent
 * and its items read as one object with a heading rather than two stacked
 * ones. */
.package-block[open] > .package-row { border-bottom: 1px solid var(--border-strong); }
/* ...and the band closes on the last item rather than on a rule that has
 * nothing under it. */
.package-items > .punch-row:last-child { border-bottom: none; }

@media (max-width: 900px) {
  /* §3.3's way in, which the desktop hides because the row is the way in
   * there. Full width: a 106px button under a stack of full-width cards is
   * the same orphan it was on the desktop. */
  .package-open-foot { display: flex; padding: 2px 10px 10px; }
  .package-open.button-outline { flex: 1; justify-content: center; }

  /* A CARD INSIDE A CARD IS TWO OBJECTS WHERE THERE IS ONE. At this width the
   * table is already cards, so a packaged item was a bordered, rounded,
   * 8px-separated card sitting inside the package's own bordered, rounded
   * card — which read as a list of items that happened to be near a package.
   * Inside the block they give up their chrome and become rows on the band,
   * with the spine down the side of all of them. */
  .package-items > .punch-row {
    margin-bottom: 0;
    background: transparent;
    border: none;
    border-top: 1px solid var(--border-strong);
    border-radius: 0;
  }
  .package-items > .punch-row:first-child { border-top: none; }
}

/* The trace under "Punch list complete" (§ punch list gate) -----------------
 *
 * A box ticked over a warning is a different fact from a box ticked, and this
 * is the only thing on the step that says which happened. Rust, because it is
 * the colour this app uses where something needs knowing rather than
 * celebrating — not an error, and not invisible either. */
/* .sub is a flex row and does not wrap, so the trace needs the row's
 * permission to take a line of its own. Scoped with :has so the other
 * eighty-odd substep rows keep the layout they have. */
.sub:has(.sub-override) { flex-wrap: wrap; }
.sub-override {
  flex-basis: 100%;
  margin: 2px 0 0 30px;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--rust);
}

/* Who gets package email (E1) ----------------------------------------------
 *
 * A setting on a row of people, not an action: a checkbox beside the state,
 * ahead of the buttons that do things. Deactivated rows do not show it — the
 * mail stops because the account did, and a switch that changes nothing is
 * worse than no switch at all. */
.person-notify { display: flex; align-items: center; }
.notify-toggle {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px;
  font-size: 12.5px; color: var(--text-secondary);
  cursor: pointer;
}
.notify-toggle input { width: 16px; height: 16px; accent-color: var(--rust); }
.notify-toggle input:disabled { opacity: 0.5; }

/* Mail that did not arrive (E1) --------------------------------------------
 *
 * Rust on a warm tint, above the outcome block. Not an error banner — nothing
 * is broken — but the one thing on this screen that somebody has to act on
 * outside the app, so it is the first thing read. */
.mail-trouble {
  margin-bottom: 14px; padding: 12px 14px;
  background: #EAD8D1; border-radius: var(--r-md);
  font-size: 13px; line-height: 1.5; color: var(--rust);
}
.mail-trouble-head { margin: 0 0 6px; font-weight: 600; }
.mail-trouble ul { margin: 0; padding-left: 18px; }
.mail-trouble li + li { margin-top: 4px; }
.mail-trouble-foot { margin: 8px 0 0; font-weight: 500; }
