/* ===========================================================================
   Forging Solutions — forged-fired industrial design system.
   Dark charcoal surfaces, copper actions, steel hairlines. Flat (no gradients
   or shadows); depth comes from layered dark panels + steel borders.
   =========================================================================== */

:root {
  /* Surfaces */
  --bg: #14110E;
  --panel: #1B1713;
  --panel-2: #211C18;
  --border: #332D27;
  --border-strong: #4A433B;

  /* Accents — copper is for ACTIONS/EMPHASIS only, never decoration */
  --copper: #C4632C;
  --copper-bright: #D98A45;

  /* Metal + text */
  --steel: #BFB8AC;
  --text: #ECE6DD;
  --muted: #A69E92;
  --price: #F2ECE3;

  /* Type */
  --font-wordmark: "Cinzel", Georgia, serif;
  --font-display: "Barlow Condensed", "Arial Narrow", sans-serif;
  --font-body: "Inter", system-ui, sans-serif;

  /* Layout */
  --maxw: 68rem;
  --gap: 1rem;
  --radius: 4px;
  --header-h: 4rem;
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.6;
  font-weight: 400;
}

img, svg { max-width: 100%; display: block; }

a { color: var(--copper); text-decoration: none; }
a:hover { color: var(--copper-bright); }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  line-height: 1.05;
  margin: 0 0 0.5em;
  color: var(--text);
}

p { margin: 0 0 1rem; }
.muted { color: var(--muted); }

/* Visible focus rings everywhere */
:focus-visible {
  outline: 2px solid var(--copper-bright);
  outline-offset: 2px;
}

.container {
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 1.25rem;
}

/* --- Wordmark (Cinzel ONLY) --- */
.wordmark {
  font-family: var(--font-wordmark);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--steel);
  font-size: 1.05rem;
  line-height: 1;
  white-space: nowrap;
}

/* --- Buttons --- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 1.05rem;
  padding: 0.7rem 1.4rem;
  border-radius: var(--radius);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.btn-primary {
  background: var(--copper);
  color: #14110E;
  border-color: var(--copper);
}
.btn-primary:hover { background: var(--copper-bright); border-color: var(--copper-bright); color: #14110E; }
.btn-ghost {
  background: transparent;
  color: var(--text);
  border-color: var(--border-strong);
}
.btn-ghost:hover { border-color: var(--steel); color: var(--text); }

/* --- Header --- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
}
.header-inner {
  display: flex;
  align-items: center;
  gap: 1rem;
  min-height: var(--header-h);
}
.brand { display: inline-flex; align-items: center; gap: 0.6rem; }
.logo-slot { width: 2.5rem; height: 2.5rem; flex: none; }
.logo-slot svg,
.logo-slot img { width: 100%; height: 100%; object-fit: contain; border-radius: 3px; }

.main-nav { margin-left: auto; }
.main-nav ul {
  display: flex;
  gap: 1.25rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
.main-nav a {
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  font-size: 0.95rem;
  color: var(--muted);
}
.main-nav a:hover { color: var(--text); }

.header-phone {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--steel);
  white-space: nowrap;
}
.header-cta { flex: none; }

/* Hamburger toggle — hidden on desktop, shown below the breakpoint */
.nav-toggle { display: none; }
.nav-toggle-bars,
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  content: "";
  display: block;
  width: 1.15rem;
  height: 2px;
  background: currentColor;
  border-radius: 2px;
  transition: transform 0.15s ease, opacity 0.15s ease, background-color 0.15s ease;
}
.nav-toggle-bars { position: relative; }
.nav-toggle-bars::before { position: absolute; left: 0; top: -6px; }
.nav-toggle-bars::after { position: absolute; left: 0; top: 6px; }

/* Mobile: collapse nav + actions behind an accessible hamburger */
@media (max-width: 52rem) {
  .header-inner {
    flex-wrap: wrap;
    gap: 0.75rem;
    min-height: auto;
    padding-top: 0.55rem;
    padding-bottom: 0.55rem;
  }
  /* Shrink the wordmark so brand + toggle always fit a phone bar */
  .wordmark { font-size: 0.95rem; letter-spacing: 0.06em; }

  .nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: auto;
    flex: none;
    width: 2.75rem;
    height: 2.75rem;
    color: var(--text);
    background: transparent;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    cursor: pointer;
  }

  /* Closed: only brand + toggle show */
  .main-nav,
  .header-phone,
  .header-cta { display: none; }

  /* Open: stack links, tap-to-call, and the CTA full-width below the bar */
  .site-header.nav-open .main-nav {
    display: block;
    order: 3;
    width: 100%;
    margin-left: 0;
  }
  .site-header.nav-open .main-nav ul { flex-direction: column; gap: 0; }
  .site-header.nav-open .main-nav a {
    display: block;
    padding: 0.85rem 0.25rem;
    font-size: 1.05rem;
    color: var(--text);
    border-top: 1px solid var(--border);
  }
  .site-header.nav-open .header-phone {
    display: flex;
    align-items: center;
    order: 4;
    width: 100%;
    min-height: 2.75rem;
    padding: 0.35rem 0.25rem;
    font-size: 1.1rem;
    border-top: 1px solid var(--border);
  }
  .site-header.nav-open .header-cta {
    display: flex;
    order: 5;
    width: 100%;
    margin-top: 0.25rem;
  }

  .site-header.nav-open .nav-toggle-bars { background: transparent; }
  .site-header.nav-open .nav-toggle-bars::before { transform: translateY(6px) rotate(45deg); }
  .site-header.nav-open .nav-toggle-bars::after { transform: translateY(-6px) rotate(-45deg); }
}

/* --- Services dropdown (desktop menu + mobile accordion) --- */
.nav-services { position: relative; }
.nav-services-toggle {
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  font-size: 0.95rem;
  color: var(--muted);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}
.nav-services-toggle:hover,
.nav-services:hover .nav-services-toggle,
.nav-services:focus-within .nav-services-toggle { color: var(--text); }
.nav-caret {
  width: 0.4rem;
  height: 0.4rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translateY(-2px);
  transition: transform 0.15s ease;
}
.nav-services:hover .nav-caret,
.nav-services:focus-within .nav-caret,
.nav-services-toggle[aria-expanded="true"] .nav-caret { transform: rotate(-135deg) translateY(2px); }

.services-menu { list-style: none; margin: 0; padding: 0.4rem; }
.services-menu a,
.services-menu .svc-soon {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.55rem 0.65rem;
  border-radius: 3px;
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  font-size: 0.9rem;
  white-space: nowrap;
}
.services-menu a { color: var(--steel); }
.services-menu a:hover { background: var(--panel); color: var(--copper-bright); }
.services-menu .svc-soon { color: var(--muted); cursor: default; }
.soon-tag {
  font-family: var(--font-body);
  font-weight: 500;
  text-transform: none;
  letter-spacing: 0.02em;
  font-size: 0.65rem;
  padding: 0.08rem 0.45rem;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  color: var(--muted);
}
.services-menu-sep { border-top: 1px solid var(--border); margin-top: 0.3rem; padding-top: 0.3rem; }
.services-menu-sep a { color: var(--copper); font-size: 0.82rem; }

/* Desktop: absolute dropdown revealed on hover / focus-within / expanded.
   `.nav-services .services-menu` (0,2,0) beats `.main-nav ul` (0,1,1) so the
   submenu stacks vertically instead of inheriting the nav's horizontal flex. */
@media (min-width: 52.01rem) {
  .nav-services .services-menu {
    display: block;
    position: absolute;
    top: 100%;
    left: 0;
    margin-top: 0.6rem;
    min-width: 15rem;
    background: var(--panel-2);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-4px);
    transition: opacity 0.14s ease, transform 0.14s ease, visibility 0.14s;
    z-index: 60;
  }
  .nav-services:hover .services-menu,
  .nav-services:focus-within .services-menu,
  .nav-services-toggle[aria-expanded="true"] + .services-menu {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
  }
  /* hover bridge so the menu doesn't drop while moving the cursor to it */
  .nav-services::after { content: ""; position: absolute; top: 100%; left: 0; right: 0; height: 0.6rem; }
}

/* Mobile: accordion inside the hamburger menu */
@media (max-width: 52rem) {
  .nav-services { width: 100%; }
  .nav-services-toggle {
    display: flex;
    justify-content: space-between;
    width: 100%;
    padding: 0.85rem 0.25rem;
    font-size: 1.05rem;
    color: var(--text);
    border-top: 1px solid var(--border);
  }
  .nav-caret { transform: rotate(45deg) translateY(-2px); }
  .site-header.nav-open .services-menu { display: none; padding: 0.1rem 0 0.5rem 0.9rem; }
  .site-header.nav-open .nav-services-toggle[aria-expanded="true"] + .services-menu { display: block; }
  .site-header.nav-open .services-menu a,
  .site-header.nav-open .services-menu .svc-soon {
    border-top: 0;
    padding: 0.65rem 0.25rem;
    font-size: 1rem;
    color: var(--steel);
  }
  .site-header.nav-open .services-menu-sep a { color: var(--copper); font-size: 0.95rem; }
}

/* --- Sections --- */
/* PAGE BANDS ARE THE MARKETING SITE'S, NOT THE ADMIN'S (0140). This used to be
   a bare `section { padding: 3.5rem 0 }`, and every <section> inside the admin
   inherited 56px of dead air above and below: the dashboard's Add row was
   180px tall for a 44px row of buttons, and five admin rules existed only to
   undo it (.sch-group, .dash-appt-sec, .pj-sec, .kc-hiring, .forge-section).
   :where() keeps this at the old specificity (one element), so every public
   rule that overrode it still does, and inside body.admin-app nothing inherits
   it. Admin sections that want spacing say so themselves, below and in their
   own families. The five `padding: 0` workarounds went in 0141. */
:where(body:not(.admin-app)) section { padding: 3.5rem 0; }
.section-eyebrow {
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  font-size: 0.8rem;
  color: var(--muted);
  margin-bottom: 0.75rem;
}
.section-title { font-size: clamp(1.6rem, 4vw, 2.2rem); }

/* --- Hero --- */
.hero { padding-top: 3rem; padding-bottom: 3rem; border-bottom: 1px solid var(--border); }

/* Photographic hero: image layer + charcoal overlay + copy on top */
.hero-photo {
  position: relative;
  overflow: hidden;
  background: var(--bg); /* fallback while the photo loads */
  display: flex;
  align-items: center;
  min-height: 30rem;
}
.hero-bg { position: absolute; inset: 0; z-index: 0; }
.hero-bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 45%;
  display: block;
}
.hero-photo::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
    180deg,
    rgba(20, 17, 14, 0.86) 0%,
    rgba(20, 17, 14, 0.62) 55%,
    rgba(20, 17, 14, 0.74) 100%
  );
}
.hero-inner { position: relative; z-index: 2; width: 100%; }
.hero-copy { max-width: 40rem; }

.hero-headline {
  font-size: clamp(2.5rem, 7vw, 4.5rem);
  line-height: 0.95;
  margin: 0 0 1rem;
}
.hero-tagline {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--copper);
  font-size: clamp(1.1rem, 2.5vw, 1.5rem);
  margin: 0 0 1rem;
}
.hero-sub { color: var(--muted); font-size: 1.05rem; max-width: 34rem; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 0.85rem; margin-top: 1.5rem; }
@media (max-width: 52rem) {
  .hero-photo { min-height: 32rem; }
}

/* --- Rate plate (flyer-style) --- */
.rates { background: var(--panel); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.rate-plate {
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  overflow: hidden;
  max-width: 42rem;
  background: var(--panel-2);
}
.rate-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 1rem;
  padding: 0.9rem 1.25rem;
  border-top: 1px solid var(--border);
  align-items: baseline;
}
.rate-row:first-child { border-top: none; }
.rate-head {
  background: var(--copper);
  color: #14110E;
  border-top: none;
}
.rate-head span {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-size: 0.95rem;
}
.rate-duration {
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  font-size: 1.15rem;
  color: var(--text);
}
.rate-price {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.5rem;
  color: var(--price);
  text-align: right;
}
.rate-note {
  color: var(--muted);
  font-size: 0.95rem;
  max-width: 42rem;
  margin-top: 1rem;
}

/* Phones: tighten the rate plate so the duration/price rows stay readable
   with no side-scroll */
@media (max-width: 30rem) {
  .rate-row { padding: 0.7rem 0.9rem; gap: 0.75rem; }
  .rate-duration { font-size: 1.05rem; }
  .rate-price { font-size: 1.3rem; }
  .rate-head span { font-size: 0.85rem; }
}

/* --- How it works --- */
.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 1.25rem;
}
.step {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.25rem;
}
.step-num {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.4rem;
  color: var(--copper);
  line-height: 1;
  margin-bottom: 0.5rem;
}
.step h3 { font-size: 1.15rem; margin-bottom: 0.35rem; }
.step p { color: var(--muted); margin: 0; font-size: 0.98rem; }

/* --- Recent work strip --- */
.rw-lead { max-width: 42rem; margin-bottom: 0.5rem; }
.work-grid {
  list-style: none;
  margin: 1.5rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, 1fr); /* 2-up on mobile */
  gap: 1rem;
}
.work-card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.work-card img {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  display: block;
  transition: opacity 0.2s ease;
}
.work-card:hover { border-color: var(--copper); }
.work-card:hover img { opacity: 0.9; }
@media (min-width: 52rem) {
  .work-grid { grid-template-columns: repeat(3, 1fr); gap: 1.25rem; } /* 3-up desktop */
}

/* --- Projects gallery (/projects/) + homepage teaser --- */
.projects-grid {
  list-style: none;
  margin: 2rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr; /* 1-up on small mobile */
  gap: 1.25rem;
}
@media (min-width: 34rem) {
  .projects-grid { grid-template-columns: repeat(2, 1fr); } /* 2-up */
}
@media (min-width: 52rem) {
  .projects-grid { grid-template-columns: repeat(3, 1fr); } /* 3-up desktop */
}
.project-card {
  display: block;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  color: var(--text);
}
.project-card:hover { border-color: var(--copper); color: var(--text); }
.project-card img {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  transition: opacity 0.2s ease;
}
.project-card:hover img { opacity: 0.9; }
.project-cap { display: block; padding: 0.85rem 1rem 1rem; }
.project-title {
  display: block;
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  font-size: 1.05rem;
  line-height: 1.1;
}
.project-desc { display: block; color: var(--muted); font-size: 0.92rem; margin-top: 0.25rem; }

/* --- ONE PROJECT'S BEFORE/AFTER PAIRS (0132c) ---------------------------------
   Decision D10: the pair reads side by side, because that IS the comparison —
   stacking them on a phone would put the "after" a scroll away from the
   "before" and lose the point. Two columns at every width; at 390 each shot is
   about 170px wide, which is enough to see the change and taps through to the
   full image. */
.pj-capped { margin: 0.5rem 0 0; }
/* 0132d — a project's own page: the way back, at the top and at the foot. */
.pj-back { color: var(--copper); text-decoration: none; font-weight: 600; }
.pj-back:hover { color: var(--copper-bright); }
.pj-back-foot { margin: 2rem 0 0; }
.project-card { scroll-margin-top: 1.5rem; }
.pj-project { padding: 2.5rem 0 0; scroll-margin-top: 1.5rem; }
.pj-project-title { margin: 0 0 0.15rem; font-size: 1.35rem; }
.pj-project-meta { margin: 0 0 0.35rem; color: var(--muted); }
.pj-project-desc { margin: 0 0 1rem; max-width: 62ch; }
.pj-pairs { list-style: none; margin: 0; padding: 0; display: grid; gap: 1.25rem; }
.pj-pair {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.6rem; align-items: start;
}
.pj-shot { margin: 0; min-width: 0; }
.pj-shot a { display: block; }
.pj-shot img {
  display: block; width: 100%; height: auto; aspect-ratio: 4 / 3; object-fit: cover;
  border: 1px solid var(--border); border-radius: var(--radius); background: var(--panel);
}
.pj-shot figcaption {
  margin-top: 0.3rem; font-family: var(--font-display); text-transform: uppercase;
  letter-spacing: 0.06em; font-size: 0.78rem; color: var(--muted);
}
.pj-shot--after figcaption { color: var(--copper); }
@media (min-width: 46rem) {
  .pj-pair { gap: 1rem; }
  .pj-project-title { font-size: 1.6rem; }
}

/* Teaser: a heading row with a "see all" link on the homepage */
.projects-teaser-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.projects-teaser-head .see-all { white-space: nowrap; font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; }

/* --- Lightbox (progressive enhancement over the real image links) --- */
body.lightbox-open { overflow: hidden; }
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}
.lightbox[hidden] { display: none; }
.lightbox-backdrop { position: absolute; inset: 0; background: rgba(10, 8, 6, 0.9); }
.lightbox-figure {
  position: relative;
  z-index: 1;
  margin: 0;
  max-width: min(92vw, 1100px);
  display: flex;
  flex-direction: column;
  align-items: center;
}
.lightbox-img {
  max-width: 100%;
  max-height: 80vh;
  width: auto;
  height: auto;
  object-fit: contain;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
}
.lightbox-cap { color: var(--steel); font-size: 0.95rem; text-align: center; margin-top: 0.6rem; }
.lightbox-close {
  position: absolute;
  top: -0.75rem;
  right: -0.75rem;
  width: 2.5rem;
  height: 2.5rem;
  font-size: 1.6rem;
  line-height: 1;
  color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  cursor: pointer;
}
.lightbox-close:hover { border-color: var(--copper); color: var(--copper-bright); }

/* --- Why-us / service band --- */
.band { background: var(--panel); border-top: 1px solid var(--border); }
.band-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 1.5rem;
}
.band h3 { font-size: 1.15rem; margin-bottom: 0.4rem; }
.band p { color: var(--muted); margin: 0; }

/* --- Brand band: full forged logo banner, centered + contained on dark bg --- */
.brand-band { background: var(--bg); border-top: 1px solid var(--border); text-align: center; }
.brand-band .brand-logo {
  width: 100%;
  max-width: 760px;
  height: auto;
  margin: 0 auto;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
}

/* --- Service hero (bin photo + forged text overlay) --- */
.svc-hero {
  position: relative;
  overflow: hidden;
  background: var(--panel-2);
  border-bottom: 1px solid var(--border);
}
.svc-hero-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  z-index: 0;
}
/* Left-weighted + bottom scrim so the forged eyebrow/H1/crumb stay legible
   over the photo (mirrors the home hero's darkening treatment). */
.svc-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    linear-gradient(90deg, rgba(20, 17, 14, 0.94) 0%, rgba(20, 17, 14, 0.82) 42%, rgba(20, 17, 14, 0.45) 100%),
    linear-gradient(0deg, rgba(20, 17, 14, 0.65) 0%, rgba(20, 17, 14, 0) 55%);
}
.svc-hero-inner {
  position: relative;
  z-index: 2;
  min-height: clamp(300px, 34vw, 380px);
  padding: 2.25rem 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.svc-hero-inner h1 { font-size: clamp(1.9rem, 5vw, 3rem); margin-bottom: 0; }
.svc-hero-inner .section-eyebrow { margin-bottom: 0.4rem; }
.svc-hero-inner .hero-actions { margin-top: 1.25rem; }

.svc-intro { padding: 2rem 0 0.5rem; }
.svc-intro .lead { margin: 0 0 1rem; }
.svc-intro p:last-child { margin-bottom: 0; }
/* Owner-provided local proof callout (rendered only when filled in the data). */
.owner-highlight {
  border-left: 3px solid var(--copper);
  padding: 0.4rem 0 0.4rem 1rem;
  margin-top: 1.25rem;
  color: var(--steel);
  font-style: italic;
}

/* --- Itemized service list (handyman hub) --- */
.svc-list {
  list-style: none;
  margin: 1.5rem 0 1rem;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 1.5rem;
}
@media (min-width: 34rem) { .svc-list { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 52rem) { .svc-list { grid-template-columns: repeat(3, 1fr); } }
.svc-list li {
  position: relative;
  padding: 0.6rem 0.5rem 0.6rem 1.6rem;
  border-bottom: 1px solid var(--border);
  color: var(--text);
  font-size: 0.98rem;
}
.svc-list li::before {
  content: "";
  position: absolute;
  left: 0.3rem;
  top: 1.05rem;
  width: 0.5rem;
  height: 0.5rem;
  background: var(--copper);
  border-radius: 1px;
  transform: rotate(45deg);
}
.svc-scope { max-width: 46rem; margin-top: 0.75rem; }

/* Two-column plain list (city "what we handle") */
.plain-list.two-col { grid-template-columns: 1fr; }
@media (min-width: 30rem) { .plain-list.two-col { grid-template-columns: 1fr 1fr; gap: 0.4rem 1.25rem; } }

/* --- Breadcrumbs (service/city pages) --- */
.breadcrumbs { margin-bottom: 0.75rem; }
.breadcrumbs ol {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0;
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  font-size: 0.8rem;
}
.breadcrumbs li { color: var(--muted); }
.breadcrumbs li + li::before { content: "/"; color: var(--border-strong); margin-right: 0.4rem; }
.breadcrumbs a { color: var(--steel); }
.breadcrumbs a:hover { color: var(--copper-bright); }
.breadcrumbs [aria-current="page"] { color: var(--text); }

/* --- City grid (dumpster-rental hub) --- */
.city-grid {
  list-style: none;
  margin: 1.5rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.85rem;
}
@media (min-width: 34rem) { .city-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 52rem) { .city-grid { grid-template-columns: repeat(3, 1fr); } }
.city-card {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  height: 100%;
  padding: 1rem 1.1rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
}
.city-card:hover { border-color: var(--copper); color: var(--text); }
.city-card-name {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  font-size: 1.1rem;
}
.city-card-meta { color: var(--muted); font-size: 0.85rem; }

/* --- City chips (sibling cross-links) --- */
.city-chips { list-style: none; padding: 0; margin: 1rem 0; display: flex; flex-wrap: wrap; gap: 0.5rem; }
.city-chips a {
  display: inline-block;
  padding: 0.35rem 0.75rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--steel);
  font-size: 0.9rem;
}
.city-chips a:hover { border-color: var(--copper); color: var(--copper-bright); }

/* --- Service-area: grouped city grid --- */
.area-group { margin-bottom: 2.75rem; }
.area-group:last-child { margin-bottom: 0; }
.area-group-head {
  display: flex;
  align-items: baseline;
  gap: 0.85rem;
  padding-bottom: 0.6rem;
  margin-bottom: 1.25rem;
  border-bottom: 1px solid var(--border);
}
.area-group-head h2 { font-size: clamp(1.3rem, 3vw, 1.6rem); margin: 0; }
.area-group-count {
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-size: 0.75rem;
  color: var(--copper);
  white-space: nowrap;
}
.area-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.85rem;
}
@media (min-width: 34rem) { .area-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 52rem) { .area-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 68rem) { .area-grid { grid-template-columns: repeat(4, 1fr); } }
.area-card {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  padding: 1rem 1.1rem 0.9rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: border-color 0.15s ease;
}
.area-card:hover { border-color: var(--copper); }
.area-card-top { display: flex; flex-direction: column; gap: 0.3rem; }
.area-card-name {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  font-size: 1.15rem;
  color: var(--text);
  line-height: 1.1;
}
.area-chip {
  align-self: flex-start;
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 0.66rem;
  color: var(--muted);
  padding: 0.12rem 0.5rem;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
}
.area-card-services {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--border);
}
.area-svc-link {
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  font-size: 0.8rem;
  color: var(--copper);
}
.area-svc-link:hover { color: var(--copper-bright); }
.area-svc-link span { display: inline-block; transition: transform 0.15s ease; }
.area-card:hover .area-svc-link span { transform: translateX(3px); }

/* --- Homepage: service-area teaser band --- */
.area-teaser-groups {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.25rem;
  margin-top: 1.5rem;
}
@media (min-width: 40rem) { .area-teaser-groups { grid-template-columns: repeat(3, 1fr); } }
.area-teaser-group h3 {
  font-size: 1rem;
  margin-bottom: 0.5rem;
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--border-strong);
  color: var(--steel);
}
.area-teaser-group p { color: var(--muted); font-size: 0.95rem; margin: 0; line-height: 1.7; }

/* --- Footer (multi-column with service areas) --- */
.footer-grid { display: grid; grid-template-columns: 1fr; gap: 2rem; }
@media (min-width: 46rem) { .footer-grid { grid-template-columns: 1.1fr 2fr 1fr; gap: 2.5rem; } }
.footer-brand .wordmark { font-size: 1.1rem; }
.footer-brand p { color: var(--muted); margin: 0.6rem 0; }
.footer-phone { font-family: var(--font-display); font-weight: 700; letter-spacing: 0.02em; color: var(--steel); font-size: 1.05rem; }
.footer-heading {
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.8rem;
  color: var(--copper);
  margin: 0 0 0.9rem;
}
.footer-city-cols {
  list-style: none;
  margin: 0 0 0.85rem;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.4rem 1.25rem;
}
@media (min-width: 34rem) { .footer-city-cols { grid-template-columns: repeat(3, 1fr); } }
.footer-city-cols a,
.footer-links a { color: var(--muted); font-size: 0.92rem; }
.footer-city-cols a:hover,
.footer-links a:hover { color: var(--copper-bright); }
.footer-all { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.8rem; color: var(--copper); }
.footer-links ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.45rem; }

/* --- Guides: article prose + index cards --- */
.guide-meta { color: var(--muted); font-size: 0.9rem; margin-top: 0.5rem; }
.guide-section { padding: 2rem 0 3rem; }
.prose { font-size: 1.05rem; line-height: 1.75; color: var(--text); }
.prose > p { margin: 0 0 1.15rem; }
.prose > :first-child { margin-top: 0; }
.prose h2 { font-size: clamp(1.4rem, 3.5vw, 1.85rem); margin: 2.25rem 0 0.85rem; }
.prose h3 { font-size: 1.2rem; margin: 1.75rem 0 0.6rem; color: var(--steel); }
.prose ul, .prose ol { margin: 0 0 1.15rem; padding-left: 1.35rem; }
.prose li { margin-bottom: 0.5rem; color: var(--text); }
.prose a { color: var(--copper); text-decoration: underline; text-underline-offset: 2px; }
.prose a:hover { color: var(--copper-bright); }
.prose strong { color: var(--text); }

.guide-grid { list-style: none; margin: 1.5rem 0 0; padding: 0; display: grid; grid-template-columns: 1fr; gap: 1rem; }
@media (min-width: 40rem) { .guide-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 60rem) { .guide-grid { grid-template-columns: repeat(3, 1fr); } }
.guide-card { display: flex; flex-direction: column; gap: 0.5rem; height: 100%; padding: 1.25rem; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); }
.guide-card:hover { border-color: var(--copper); color: var(--text); }
.guide-card-date { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.72rem; color: var(--copper); }
.guide-card-title { font-family: var(--font-display); font-weight: 700; text-transform: uppercase; letter-spacing: 0.01em; font-size: 1.25rem; line-height: 1.15; }
.guide-card-excerpt { color: var(--muted); font-size: 0.95rem; line-height: 1.55; }
.guide-card-cta { margin-top: auto; font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.85rem; color: var(--copper); }

/* --- Footer --- */
.site-footer {
  background: var(--panel-2);
  border-top: 1px solid var(--border);
  padding: 2rem 0;
  color: var(--muted);
  font-size: 0.95rem;
}
.footer-inner { display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; align-items: center; justify-content: space-between; }
.footer-inner a { color: var(--steel); }
/* The site credit (footer.njk's bottom line): the quietest text in the
   footer — smaller than the muted links, inheriting their colour, set off by
   the same rule the footer's top border uses. The name never breaks across
   lines; the sentence may wrap before it on a narrow screen, never inside it. */
.footer-bottom { margin-top: 1.75rem; padding-top: 1rem; border-top: 1px solid var(--border); }
.footer-credit { margin: 0; font-size: 0.75rem; letter-spacing: 0.02em; line-height: 1.5; color: var(--muted); }
.footer-credit a { color: inherit; white-space: nowrap; text-decoration: underline; text-underline-offset: 2px; }
.footer-credit a:hover { color: var(--copper-bright); }

/* --- Utility for interim/stub pages --- */
.stub { padding: 3.5rem 0; }
.stub .container > p { color: var(--muted); }

/* ===========================================================================
   Forms + booking wizard
   =========================================================================== */
.container-narrow { max-width: 40rem; }
.book-section { padding: 2.5rem 0 4rem; }
.wizard-head { margin-bottom: 1.5rem; }
.wizard-head h1 { font-size: clamp(1.8rem, 5vw, 2.6rem); }

label {
  display: block;
  margin: 1rem 0 0.35rem;
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  font-size: 0.9rem;
  color: var(--steel);
}
label.checkbox {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  text-transform: none;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 1rem;
  color: var(--text);
  letter-spacing: 0;
}
.optional { color: var(--muted); font-weight: 400; }

input[type="text"], input[type="email"], input[type="tel"],
input[type="date"], input[type="time"], input[type="number"],
input[type="password"], input[type="search"],
select, textarea {
  width: 100%;
  padding: 0.7rem 0.8rem;
  font-size: 1rem;
  font-family: var(--font-body);
  color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  min-height: 3rem;
}
input::placeholder, textarea::placeholder { color: var(--muted); }
/* `date` and `time` both draw native chrome — a calendar picker and a clock —
   and both need telling which way round the theme goes, or the browser paints a
   light control with a light icon on the dark panel. `time` was omitted from
   both this rule and the one above, which is the whole reason the Start field
   arrived as a stark white box beside a correctly dark Date field. */
input[type="date"], input[type="time"], select { color-scheme: dark; }
textarea { min-height: 6rem; resize: vertical; line-height: 1.5; }

/* Honeypot — parked far off-screen, out of the tab order, no pointer target.
   Real users never see or fill it; a submitted value flags a bot server-side.

   This was the standard visually-hidden utility (1px + clip), which is the
   pattern for content screen readers SHOULD reach. A honeypot is the opposite:
   nothing should reach it. It is parked at -9999px instead, and the field's
   name/id/label carry no meaning for autofill to match — that naming, not this
   rule, was what let password managers fill it and cost real leads. CSS alone
   never stopped autofill and is not what is relied on here. */
.hp-field {
  position: absolute !important;
  left: -9999px !important;
  top: auto;
  width: 1px; height: 1px;
  padding: 0; margin: 0; border: 0; overflow: hidden;
  white-space: nowrap; pointer-events: none;
}
input[type="checkbox"] {
  width: 1.25rem; height: 1.25rem; margin-top: 0.15rem;
  accent-color: var(--copper); flex: none;
}
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }

/* Progress */
.progress { margin-bottom: 1.25rem; }
.progress-meta {
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.8rem;
  color: var(--muted);
  margin-bottom: 0.4rem;
}
.progress-track { height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; }
.progress-fill { height: 100%; width: 12.5%; background: var(--copper); transition: width 0.2s ease; }

/* Running price summary */
.price-summary {
  display: flex; justify-content: space-between; align-items: baseline;
  padding: 0.75rem 1rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 1.5rem;
}
.ps-label {
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.05em; font-size: 0.9rem; color: var(--muted);
}
.ps-total { font-family: var(--font-display); font-weight: 700; font-size: 1.5rem; color: var(--price); }

/* Steps */
.step h2 { font-size: 1.4rem; margin-bottom: 1rem; }
.step-nav { display: flex; gap: 0.75rem; margin-top: 1.75rem; flex-wrap: wrap; }
.step-nav .btn { flex: 1 1 auto; min-height: 3rem; }
/* Client modal: the optional structured-name group, folded away so the common
   case stays a short form. Styled as a quiet disclosure, not a panel. */
.cf-name-details { margin: 0.1rem 0 0.2rem; }
.cf-name-details > summary {
  cursor: pointer; color: var(--muted); font-size: 0.92rem;
  padding: 0.35rem 0; list-style-position: inside;
}
.cf-name-details > summary:hover { color: var(--text); }
.cf-optional { opacity: 0.75; }
.cf-hint {
  margin: 0.1rem 0 0.6rem; color: var(--muted);
  font-size: 0.86rem; line-height: 1.5;
}

/* Client modal: address autocomplete. The wrapper anchors the suggestion list
   under the input; the list is absolutely positioned so it overlays the fields
   below rather than pushing the form around as you type. */
.addr-ac-wrap { position: relative; display: block; }
.addr-suggest {
  position: absolute; z-index: 20; left: 0; right: 0; top: 100%;
  margin: 0.2rem 0 0; padding: 0.25rem; list-style: none;
  max-height: 15rem; overflow-y: auto;
  background: var(--panel); border: 1px solid var(--copper);
  border-radius: var(--radius); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
}
.addr-suggest-item {
  padding: 0.5rem 0.6rem; border-radius: calc(var(--radius) - 2px);
  cursor: pointer; line-height: 1.35;
}
.addr-suggest-item:hover,
.addr-suggest-item.is-active { background: rgba(196, 99, 44, 0.16); }
.addr-suggest-main { display: block; font-size: 0.95rem; }
.addr-suggest-sub { display: block; color: var(--muted); font-size: 0.82rem; }
.cf-addr-row { display: grid; grid-template-columns: 5rem 1fr; gap: 0.6rem; }

.step-note { color: var(--muted); }
/* Advisory note that sits ABOVE a field — advice, not a warning, so it borrows
   the copper left-accent of .overage-panel at a lower volume rather than the
   filled copper of .step-error. */
.step-tip {
  margin-bottom: 1.1rem; padding: 0.7rem 0.9rem;
  color: var(--muted); font-size: 0.94rem; line-height: 1.55;
  background: var(--panel);
  border-left: 2px solid var(--copper);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.step-error {
  margin-top: 1rem; padding: 0.6rem 0.8rem;
  color: #F2B8A0; background: rgba(196, 99, 44, 0.12);
  border: 1px solid var(--copper); border-radius: var(--radius);
  font-weight: 500;
}

/* Tier cards */
.tier-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 0.75rem; }
.tier-card {
  display: flex; flex-direction: column; gap: 0.35rem; text-align: left;
  padding: 1rem; min-height: 5.5rem;
  background: var(--panel-2); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--radius); cursor: pointer;
}
.tier-card[aria-checked="true"] { border-color: var(--copper); box-shadow: inset 0 0 0 1px var(--copper); }
.tc-label { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.02em; font-size: 1.05rem; }
.tc-price { font-family: var(--font-display); font-weight: 700; font-size: 1.4rem; color: var(--price); }

/* Ton selector */
.ton-selector { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.ton-option {
  display: flex; flex-direction: column; align-items: center; gap: 0.25rem;
  min-width: 5rem; min-height: 4.5rem; padding: 0.75rem 1rem;
  background: var(--panel-2); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--radius); cursor: pointer;
}
.ton-option[aria-checked="true"] { border-color: var(--copper); box-shadow: inset 0 0 0 1px var(--copper); }
.to-n { font-family: var(--font-display); font-weight: 700; font-size: 1.6rem; }
.to-cost { font-size: 0.85rem; color: var(--muted); }

/* Order summary */
.order-summary { border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.25rem; background: var(--panel); margin-bottom: 1.5rem; }
.order-summary dl { display: grid; grid-template-columns: auto 1fr; gap: 0.4rem 1rem; margin: 0; }
.order-summary dt { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.02em; font-size: 0.9rem; color: var(--muted); }
.order-summary dd { margin: 0; text-align: right; color: var(--text); }
.order-summary .total-row { color: var(--price); font-weight: 700; font-size: 1.15rem; border-top: 1px solid var(--border); padding-top: 0.5rem; margin-top: 0.25rem; }

/* Contract text */
.contract-text {
  white-space: pre-wrap;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem;
  max-height: 15rem; overflow-y: auto;
  font-family: var(--font-body); font-size: 0.9rem; color: var(--muted);
  margin-bottom: 1rem;
}

/* Canceled / notice banner */
.notice {
  padding: 0.8rem 1rem; margin-bottom: 1.5rem;
  background: var(--panel); border: 1px solid var(--copper); border-left-width: 3px;
  border-radius: var(--radius); color: var(--text);
}

/* Status groups on the Contracts tab. A header plus its rows, repeated — the
   list shows everything at once instead of one status at a time, so the headers
   are what tell you where you are in it.

   Reuses .inv-filter-n for the count bubble so a group header counts the same
   way the estimates tab's pills do, and .est-list / .est-item for the rows
   underneath, which are unchanged. */
.ct-group { margin: 0 0 1.5rem; }
.ct-group:last-child { margin-bottom: 0; }
.ct-group-head {
  display: flex; align-items: center; gap: 0.5rem;
  margin: 0 0 0.6rem; padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--border);
  font-family: var(--font-display); font-size: 0.95rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted);
}
/* The count sits with the label, not pushed to the far edge: at 1200px a
   right-aligned bubble ends up an inch of empty rule away from the word it
   counts. */
.ct-group-head .inv-filter-n { margin-left: 0; }

/* The selected client's contact details, under the estimate builder's picker.
   Borrows .client-fields from the Clients tab rather than inventing a second
   way to lay out a name and an address.

   The two-column card is right from tablet up, and that is unchanged. On a
   PHONE it was not: the overrides below are doubled-class (0,2,0) and a media
   query adds no specificity, so this card was the one place .client-fields
   never collapsed to a single column. The earlier note here claimed "at 360px
   the two columns are 88px + 193px, the email fits, and nothing overflows" —
   true of the short email it was measured against, false of a real one.
   kajsa.lindqvist@northernvirginiaproperties.com made the page 496px wide at
   every phone width, a 106px overflow at 390. The phone rule below restores
   the collapse; the long-string rule above stops the email setting the floor.
   Contractors and Suppliers detail use plain .client-fields and measured clean
   at 360 throughout — this card was the outlier, not the pattern. */
.est-client-card {
  margin: -0.35rem 0 1rem;
  padding: 0.75rem 0.9rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: 3px solid var(--copper);
  border-radius: var(--radius);
}
.est-client-name {
  margin: 0 0 0.5rem;
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--text);
}
/* Narrower labels than the Clients tab's 8rem: this card shows three fields,
   not nine, and sits inside a form rather than owning the screen. */
/* BOTH classes, not just .est-client-fields. The element carries
   `client-fields est-client-fields`, and a single-class selector here ties on
   specificity with .client-fields below — which wins on source order, because
   it is written later in this file. Measured that mistake before fixing it:
   the labels stayed at .client-fields' 8rem and stayed vertically centred, so
   "ADDRESS" sat beside the second line of a three-line address. Doubling the
   selector makes these rules win on specificity, so neither depends on where
   in the file it happens to sit. */
.client-fields.est-client-fields { grid-template-columns: 5.5rem 1fr; gap: 0.35rem 0.9rem; }
/* Top-align the labels: .client-fields centres them, which reads fine against
   one-line values and puts "ADDRESS" beside the middle of a multi-line
   address. */
.client-fields.est-client-fields dt { align-self: start; }
/* Doubled-class to match the 0,2,0 above — a single-class selector inside this
   media query loses to it outright, which is the bug this fixes. Same boundary
   as the plain .client-fields collapse, so the two cards behave identically
   instead of one silently opting out. The pair moves together or the
   specificity bug comes straight back.

   30rem, NOT the 45.99rem the rest of the admin's narrow rules use. Those
   widened because they had a dead zone — nothing matched 641-735 and the
   layout fell through to desktop. This card never had one: the two-column
   rule above it is correct from 480px up and was measured comfortable at 680
   (an 88px label beside a 505px value). Collapsing it there cost ~200px of
   scroll and bought nothing, so it stays a phone rule. Deliberately out of
   step with 45.99rem, and the reason is this paragraph. */
@media (max-width: 30rem) {
  .client-fields.est-client-fields { grid-template-columns: 1fr; gap: 0.15rem; }
  .client-fields.est-client-fields dd { margin-bottom: 0.5rem; }
}
.est-client-empty { margin: 0; font-size: 0.9rem; }

/* Add / Credit direction on an amendment estimate (0057). */
.est-co-dir { border: 0; margin: 0.5rem 0 0; padding: 0; }
.est-co-dir legend { padding: 0; font-size: 0.85rem; }
.est-co-dir-note { margin: 0.25rem 0 0; font-size: 0.85rem; }

/* The final-price-adjustment control on the contract detail (0056). */
.ct-adjust { margin: 0 0 1rem; }
.ct-adjust-current { margin: 0 0 0.5rem; }
.ct-adjust-current p { margin: 0 0 0.25rem; }
.ct-adjust-form { margin: 0.6rem 0 0; display: grid; gap: 0.6rem; max-width: 32rem; }

/* A negative final adjustment in the contract rollup (0056). Muted rather than
   alarm-coloured: a discount Adam chose to give is not a warning. */
.ct-rollup .is-adjust { color: var(--muted); }

/* Payment receipt opt-out in the Record-payment modal. Named .payment-receipt-*
   deliberately: .send-receipt already means the "Sent to X — timestamp"
   confirmation line under a send button, and the two must not collide. */
.payment-receipt-opt {
  display: flex; align-items: center; gap: 0.5rem;
  margin: 0.25rem 0 0; cursor: pointer;
}
.payment-receipt-opt input { margin: 0; }
.payment-receipt-note { margin: 0.2rem 0 0 1.6rem; font-size: 0.85rem; }

/* Invoice pay page (Phase 2) — the label/value list a customer reads before
   paying. Deliberately plain: an amount and what it's for, nothing else. */
.ip-lines { margin: 0 0 1.5rem; }
.ip-line { margin: 0 0 0.35rem; }
.ip-line strong { color: var(--text); }

@media (max-width: 40rem) {
  .step-nav { flex-direction: column-reverse; }
  .step-nav .btn { width: 100%; }
}

/* ===========================================================================
   Content pages (rates / service area / faq / confirmation / my-booking)
   =========================================================================== */
.page-head { padding: 2.5rem 0 1rem; border-bottom: 1px solid var(--border); }
.page-head h1 { font-size: clamp(1.8rem, 5vw, 2.8rem); }
.lead { font-size: 1.15rem; color: var(--text); max-width: 42rem; }

.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 1.25rem; }
.info-card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.5rem; }
.info-card h3 { font-size: 1.2rem; margin-bottom: 0.75rem; }
/* Contained bin photo inside an info card — 540px source stays crisp small. */
.info-card-photo .info-photo {
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  margin-bottom: 1rem;
}
.plain-list { list-style: none; padding: 0; margin: 0 0 1rem; display: grid; gap: 0.4rem; }
.plain-list li { color: var(--muted); }
.plain-list li strong { color: var(--text); }
.plain-list.prohibited li { padding-left: 1.25rem; position: relative; }
.plain-list.prohibited li::before { content: "\00d7"; position: absolute; left: 0; color: var(--copper); font-weight: 700; }

.band-cta { display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; flex-wrap: wrap; }
.band-cta .section-title { margin-bottom: 0.25rem; }

/* FAQ accordion */
.faq-list { display: grid; gap: 0.75rem; }
.faq-item { border: 1px solid var(--border); border-radius: var(--radius); background: var(--panel); overflow: hidden; }
.faq-item summary {
  cursor: pointer; list-style: none;
  padding: 1rem 1.25rem;
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.02em; font-size: 1.05rem; color: var(--text);
  display: flex; justify-content: space-between; align-items: center; gap: 1rem;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after { content: "+"; color: var(--copper); font-size: 1.4rem; line-height: 1; }
.faq-item[open] summary::after { content: "\2212"; }
.faq-item summary:focus-visible { outline: 2px solid var(--copper-bright); outline-offset: -2px; }
.faq-body { padding: 0 1.25rem 1.25rem; }
.faq-body p, .faq-body li { color: var(--muted); }
.faq-body p:last-child { margin-bottom: 0; }

.ref { font-size: 0.85rem; word-break: break-all; }

/* ===========================================================================
   Admin — utilitarian dark dashboard (plainer than the storefront)
   =========================================================================== */
/* THE STATUS-BAR INSET. Installed to a home screen, the admin runs standalone
   with apple-mobile-web-app-status-bar-style=black-translucent, which draws the
   page UNDER the iOS status bar and notch — without this, the nav's first row
   sits behind the clock.
   env(safe-area-inset-top) is 0 everywhere else (desktop, a normal browser tab,
   Android), so this max() resolves to the existing 1.5rem in every context
   except the one that needs more. It needs viewport-fit=cover to resolve at all
   — that's set on admin pages only, in base.njk.
   The bottom inset does the same for the home indicator on a gesture iPhone. */
.admin {
  padding: max(1.5rem, env(safe-area-inset-top, 0px)) 0
           max(4rem, calc(2rem + env(safe-area-inset-bottom, 0px)));
}
/* Landscape: the notch moves to a side, so the horizontal insets matter too.
   The container already has its own gutter; this only adds the difference. */
@media (orientation: landscape) {
  .admin {
    padding-left: env(safe-area-inset-left, 0px);
    padding-right: env(safe-area-inset-right, 0px);
  }
}
.admin h1 { font-size: 1.6rem; margin-bottom: 0.25rem; }
.admin-h { font-size: 1.1rem; margin: 2rem 0 0.75rem; padding-bottom: 0.4rem; border-bottom: 1px solid var(--border); }
/* 0140: the sign-in panel had its breathing room from the page-band rule;
   it keeps it, now said here. */
.admin-auth { max-width: 26rem; padding: 3.5rem 0; }
/* …and so does set-password's form (an /admin/ URL, so body.admin-app, but no
   .admin shell: it is where an invite's magic link lands). */
.admin-app #sp-form-wrap { padding: 3.5rem 0; }
.admin button {
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 0.9rem;
  padding: 0.5rem 0.9rem; min-height: 2.5rem;
  background: var(--copper); color: #14110E; border: 1px solid var(--copper);
  border-radius: var(--radius); cursor: pointer;
}
.admin button[data-prev], .admin #detail-close, .admin #sign-out,
.admin button.btn-quiet {
  background: transparent; color: var(--steel); border-color: var(--border-strong);
}
.admin button.btn-quiet:hover { border-color: var(--steel); color: var(--text); }
/* Destructive / money-moving: reads clearly different from routine copper. */
.admin button.btn-danger {
  background: transparent; color: #E79A8A; border-color: #8A4234;
}
.admin button.btn-danger:hover { background: #5B1E12; border-color: #E79A8A; color: #FBE9D0; }
.admin button:disabled { opacity: 0.5; cursor: not-allowed; }
.admin-bar { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; margin-bottom: 1rem; }
.admin-bar-actions { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }

/* Neutral loading placeholder shown until the session resolves (no flicker). */
.admin-loading { padding: 1rem 0; }

/* --- THE LONG-STRING RULE (one definition, listed application sites) -------
   A client's email address is one unbreakable 45-character token. Dropped into
   a grid or flex track it sets a min-content floor the track cannot go below,
   and the PAGE goes wider than the phone — every horizontal overflow in this
   admin traced back to exactly that, on three screens.

   `overflow-wrap: anywhere` is the specific tool, and the choice was measured,
   not guessed. On the estimate builder at 390px:
     overflow-wrap: break-word  ->  496px. NO EFFECT.
     overflow-wrap: anywhere    ->  390px. Fixed.
     word-break: break-all      ->  390px. Fixed, but breaks ordinary words
                                    mid-word even when they would have fit.
   break-word only wraps the glyphs; it does NOT shrink the min-content
   contribution, so the track stays wide and the page still overflows. `anywhere`
   shrinks it, and unlike break-all it only breaks a word that genuinely has
   nowhere else to go.

   min-width:0 goes with it because a flex/grid child defaults to min-width:auto
   — without it the box refuses to shrink even once the text inside can wrap.

   Applied by naming the value slots that carry these strings rather than by
   sprinkling the class through the markup: they are all "a contact detail
   rendered into a card", and listing them here keeps the rule in one place.
   .break-anywhere is the escape hatch for a new one. */
.break-anywhere,
.client-fields dd,
.client-row-contact,
.user-email {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* --- Cancelled / abandoned bookings (warm leads — never hidden) --- */
.cancelled-list { margin-top: 0.25rem; }
.cx-list { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); gap: 0.75rem; }
.cx-item { background: var(--panel); border: 1px solid var(--border); border-left: 3px solid var(--border-strong); border-radius: var(--radius); padding: 0.85rem; }
.cx-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.cx-head strong { color: var(--text); }
.cx-contact { margin-top: 0.35rem; font-size: 0.9rem; }
.cx-contact a { color: var(--copper-bright); }
.cx-meta { color: var(--muted); font-size: 0.9rem; margin-top: 0.25rem; }
.cx-price { color: var(--price); font-weight: 600; }
.admin .cx-open { margin-top: 0.6rem; padding: 0.35rem 0.7rem; min-height: 2.25rem; font-size: 0.8rem; }

/* --- THE ADMIN'S RESPONSIVE TOKENS (0110) -----------------------------------

   Defined once, here, and pages do not invent their own. The 0108 recon found
   fourteen per-page prefixes and twenty-four action containers, none shared;
   this is where that stops growing.

   ⚠️ THE BREAKPOINTS ARE THE ONES ALREADY IN USE, not new ones. 34rem and
   46rem are what every admin rule already keys off, including the four regimes
   of the 0077 line-editor grid that check:grid enforces. Naming them changes
   nothing today and gives the next rule one place to read them from.

   ⚠️ CUSTOM PROPERTIES DO NOT WORK IN @media, and that is a CSS fact, not an
   oversight to fix later: media queries are evaluated before the cascade, so
   `@media (min-width: var(--ws-bp-md))` matches nothing, silently. The numbers
   below are documentation for humans; the queries still spell them out, and
   these comments are how the two stay in step.
       --ws-bp-sm  34rem    phone / tablet boundary
       --ws-bp-md  46rem    tablet / desktop boundary

   THE TAP TARGET IS 44px AND IT WAS 40. .admin-nav-link carried
   min-height: 2.5rem, which is 40px — under Apple's and Google's minimum, on
   the device Adam actually uses. One token, fixed once.

   16px INPUTS, because below that iOS zooms the page on focus and leaves it
   zoomed. It is not a legibility preference; it is the difference between a
   form you can fill on a phone and one that throws you out of the layout. */
:root {
  --ws-bp-sm: 34rem;
  --ws-bp-md: 46rem;
  --ws-tap: 2.75rem;      /* 44px — the minimum touch target, everywhere */
  /* 0116: the phone's fixed bottom tab bar. It is BOTH the bar's own height and
     the space the page reserves beneath itself, stated once so the two cannot
     disagree — a bar that grew without the reservation growing would cover the
     last list row, which is the exact failure this token exists to prevent.
     The safe-area inset is added on top wherever it is used, not folded in:
     env() is 0 on every device without a home indicator, and baking a fallback
     into the token would reserve dead space on all of them. */
  --ws-tabbar-h: 3.5rem;  /* 56px — icon + label, comfortably over the 44px tap */
  --ws-input-size: 1rem;  /* 16px — below this iOS zooms on focus */
  --ws-gap-xs: 0.35rem;
  --ws-gap-sm: 0.6rem;
  --ws-gap: 1rem;
  --ws-gap-lg: 1.5rem;
}

/* Every admin control clears the tap minimum. Scoped to .admin so the public
   site's own sizing is untouched. */
.admin .btn,
.admin button:not(.btn-quiet):not(.admin-nav-link):not(.admin-tab):not(.btn-icon):not(.link-button),
.admin .admin-nav-link { min-height: var(--ws-tap); }
/* ⚠️ .admin-tab IS EXCLUDED ABOVE AND TALLER BELOW, not exempted. The phone
   bar's More slot is a <button> among four <a>s, and every .admin button rule
   would otherwise dress it as a copper pill with its own padding and its own
   40px floor — measured: it came out 97px wide beside four 73px links, which
   breaks the equal-fifths rule the bar is built on. Its height comes from
   --ws-tabbar-h (56px), which is ABOVE this minimum, so the exclusion loosens
   nothing.

   ⚠️ AND THE :not() LIST IS A VOCABULARY, NOT A PILE OF PATCHES — 0118 learned
   why it has to be. This selector is (0,4,1). A rule that tries to make one
   button small has to BEAT that, and `.admin .ph-item .lead-photo-remove`
   (0,3,0) does not: the lead photo's remove × was measured at 22 x 44px with a
   copper background, a tall orange oval over the top of the picture it was
   meant to sit on the corner of. It had been that way since 0110 wrote this
   rule at a specificity nobody rechecked the small buttons against.

   So the fix is not a fourth `.admin .thing .thing` selector in an arms race.
   `.btn-icon` is the opt-out for a control that is DELIBERATELY smaller than
   44px, and it carries a real 44px hit area with it (below) — so opting out of
   the visible minimum never means opting out of the tap target. `.link-button`
   is the other case: a button that reads as a link inside a sentence, which a
   44px floor turns into a 44px gap in a paragraph.

   ⚠️ CHECKED, NOT ASSUMED: every other rule in this file that sets
   `min-height: 0` on something inside .admin was measured. `.lead-photo-open`
   (the thumbnail) and `.el-drag` (the line-editor grip) also lose to this
   selector and BOTH are harmless — the thumbnail's own content is 52px and the
   grip is positioned top:0;bottom:0 — so neither is given an opt-out it does
   not need. */

/* THE ICON BUTTON. Small on purpose, and still a 44px target.
   ⚠️ THE HIT AREA IS A PSEUDO-ELEMENT, NOT PADDING. Padding would grow the
   visible circle, which is the whole thing being fixed; a transparent ::after
   centred on the control extends what a thumb can hit without moving a pixel
   of what an eye can see. The consumer positions the button (absolute or
   relative); this only needs SOME containing block, and sets one for the
   default case. */
.admin .btn-icon { position: relative; min-height: 0; }
.admin .btn-icon::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: var(--ws-tap); height: var(--ws-tap);
  transform: translate(-50%, -50%);
}
/* No iOS zoom-on-focus. A select is included: it zooms too. */
.admin input, .admin select, .admin textarea { font-size: max(1rem, var(--ws-input-size)); }

/* --- Admin top nav — ten tabs and a gear (0110) ------------------------------

   One partial now, where there were two: admin-nav's five links plus a copper
   Forging CTA, and forging-nav's three-group workspace nav.

   THE HEIGHT BUDGET IS THE CONSTRAINT AND IT IS MEASURED. The old flat
   eleven-link nav took 246px of a 390px screen — 37% before any content — and
   0079 found a mis-hidden More panel took it to 344px. Ten tabs do not fit one
   phone row, so below 46rem only the PHONE_PRIMARY tabs stay in the row and
   the rest live in More. tools/verify-admin-nav.mjs measures it at four widths
   and fails if the row wraps or the nav exceeds budget.

   ONE LIST, TWO PRESENTATIONS. Every tab is emitted once in the row and the
   non-primary ones a second time inside More; CSS decides which is visible at
   which width. Two lists in the template would be two lists to keep in step. */
.admin-nav {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--ws-gap-sm) var(--ws-gap); flex-wrap: wrap;
  margin: 0 0 var(--ws-gap-lg); padding: 0.5rem 0 0.75rem;
  border-bottom: 1px solid var(--border);
}
/* display:flex above beats the UA's [hidden]{display:none}, so the auth guard's
   `hidden` needs saying explicitly — otherwise signed-out visitors see the
   admin nav on every admin page. */
.admin-nav[hidden] { display: none; }
.admin-nav-links { display: flex; gap: var(--ws-gap-xs); flex-wrap: wrap; align-items: center; }
/* The phone top bar's title. Present in the markup at every width and shown
   only below 46rem, where the tabs have moved to the bottom bar and the row
   would otherwise be a gear on an empty line. */
.admin-nav-title { display: none; }
.admin-nav-link {
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 0.85rem;
  color: var(--steel); text-decoration: none;
  padding: 0.5rem 0.8rem; min-height: var(--ws-tap);
  display: inline-flex; align-items: center;
  border: 1px solid transparent; border-radius: var(--radius);
}
.admin-nav-link:hover { color: var(--text); border-color: var(--border-strong); }
.admin-nav-link.is-active {
  color: #14110E; background: var(--copper); border-color: var(--copper);
}

/* <details> as a menu: no JS, works before the auth guard resolves, keyboard
   operable for free. position:relative so the panel hangs off it on a wide
   screen without pushing the row. */
.admin-nav-more, .admin-nav-gear { position: relative; }
.admin-nav-more-btn, .admin-nav-gear-btn { list-style: none; cursor: pointer; }
.admin-nav-more-btn::-webkit-details-marker,
.admin-nav-gear-btn::-webkit-details-marker { display: none; }
.admin-nav-more-btn::after { content: " ▾"; font-size: 0.7em; opacity: 0.7; }
.admin-nav-more[open] > .admin-nav-more-btn::after { content: " ▴"; }
.admin-nav-gear-btn { gap: 0.4rem; }

/* The display:flex below beats the UA's `display:none` on a closed <details>'s
   content — the exact trap .admin-nav[hidden] documents above. Without this
   guard every hidden link stays laid out while the menu reads as shut, and the
   nav ends up TALLER than the flat list it replaced. Measured in 0079: 344px
   vs 246px at 390 before this line existed. */
.admin-nav-more:not([open]) .admin-nav-more-items,
.admin-nav-gear:not([open]) .admin-nav-gear-items { display: none; }
.admin-nav-more-items, .admin-nav-gear-items {
  display: flex; gap: 0.3rem; flex-wrap: wrap; padding: 0.35rem 0 0;
}
.admin-nav-gear-items { align-items: center; }
/* 0132b — the gear menu's two headings: Website (Projects, Traffic) and Admin. */
.admin-nav-gear-h {
  flex-basis: 100%; font-family: var(--font-display); font-size: 0.72rem;
  text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted);
  padding: 0.3rem 0.5rem 0;
}

/* BELOW 64rem: only the primaries stay in the row; the rest live in More.

   ⚠️ NOT 46rem, AND THE NUMBER IS MEASURED EVERY TIME THE LABELS CHANGE. 46rem
   is the admin's phone/tablet boundary everywhere else, and using it here put
   the whole bar in the row at 768 — a tablet Adam uses daily — where it wrapped
   to THREE rows and 168px.

   0110 measured ten tabs and set 62rem. 0115 added Invoices, and ELEVEN DO NOT
   FIT WHERE TEN DID. Re-measured in Chrome on /admin/, 12 controls (11 tabs
   plus the gear) needing 953px:

       768px   have 728px  ->  primaries + More,  65px
       900px   have 860px  ->  primaries + More,  65px
       992px   have 952px  ->  2 rows, 119px   ONE PIXEL SHORT of fitting
      1024px   have 984px  ->  1 row,   65px
      1280px   have 1048px ->  1 row,   65px

   992px missing it by a pixel is exactly why this is measured rather than
   reasoned about. The breakpoint moves to 64rem — 1024px, which is Adam's
   tablet in landscape, and the first width where eleven actually fit with room
   to spare (984 against 953). 63rem would also fit, by 15px on a 953px bar:
   under two per cent, and one longer label away from wrapping in silence.

   This is the nav's own breakpoint and deliberately not one of the shared
   tokens: it is a fact about THE WIDTH OF ELEVEN WORDS, not about a class of
   device, and it moves when the labels do. Change a label and re-run
   verify:admin, which measures one-row at four widths.

   ⚠️ nowrap, AND THE OVERFLOW CHECK IS WHAT MAKES THAT SAFE. Letting the bar
   wrap is how it grew to two rows and then three; forbidding the wrap turns the
   same mistake into a horizontal scroll, which verify:admin fails on every page
   at every width. One row or a failed build — never a quietly taller nav. */
@media (max-width: 63.99rem) {
  .admin-nav { flex-wrap: nowrap; }
  .admin-nav-links { flex: 1 1 auto; min-width: 0; }
  .admin-nav-user { flex: 0 0 auto; }
  /* 0147: the bell is a control like the gear, not a tab — it stays. */
  .admin-nav-link:not(.is-phone):not(.is-overflow):not(.admin-nav-more-btn):not(.admin-nav-gear-btn):not(.admin-bell-btn) {
    display: none;
  }
}

/* PHONE ONLY (< 46rem): THE TOP BAR IS A TITLE AND A GEAR (0116).

   Every tab has moved to the fixed bottom bar, so the row above has two jobs
   left: say which page this is, and hold the four screens you open
   deliberately. The gear keeps its icon and drops its word; it is the one
   control here that is recognisable without a label.

   ⚠️ THE HEIGHT FLOOR IS THE TAP TARGET, NOT THE TEXT, and that is why the
   saving here is smaller than it looks. The old bar was 65px: 44px of gear
   (the tap minimum, which does not negotiate) plus 0.5rem/0.75rem of padding
   plus the 1px rule. Removing the tabs removes none of that — a row containing
   one 44px control cannot be shorter than 44px. What CAN come down is the
   padding around it, and that is the whole of the measured difference.

   Measured in Chrome on /admin/ at 390px:

       before (0115)   tabs + More + gear, 0.5/0.75rem padding   65px
       after  (0116)   title + gear,       0.25/0.3rem padding   54px

   The rest of the 246px budget 4b bought is not reclaimed, it is RESPENT: the
   bottom bar costs 56px plus the home-indicator inset, and it buys four tabs
   under the thumb instead of three at arm's length. */
@media (max-width: 45.99rem) {
  .admin-nav {
    padding: 0.25rem 0 0.3rem;
    margin-bottom: var(--ws-gap);
    gap: var(--ws-gap-sm);
  }
  /* The tabs are in the bottom bar now. The row's copies stay in the markup —
     one list, three presentations — and the CSS decides which one is on. */
  .admin-nav-links { display: none; }
  .admin-nav-title {
    display: block; flex: 1 1 auto; min-width: 0;
    font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.04em; font-size: 1.05rem; color: var(--text);
    /* One line, always. A long page name must not be able to make the bar two
       rows tall — the budget this nav is measured against. */
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .admin-nav-link { padding-left: 0.6rem; padding-right: 0.6rem; }
  .admin-nav-gear-label { display: none; }
}

/* TABLET AND UP: the menus float over the page so opening one never reflows the
   content underneath. On a phone they stay in flow — an overlay that covers
   what you were reading is worse than a row that pushes it down. */
@media (min-width: 46rem) {
  .admin-nav-more-items, .admin-nav-gear-items {
    position: absolute; z-index: 20; top: calc(100% + 0.3rem); right: 0;
    flex-direction: column; min-width: 12rem; padding: 0.4rem;
    background: var(--panel-2); border: 1px solid var(--border-strong);
    border-radius: var(--radius);
  }
}

/* DESKTOP (>= 64rem): every tab is back in the row, so More has nothing to hold
   and must not render an empty affordance. */
@media (min-width: 64rem) {
  .admin-nav-more { display: none; }
}

.admin-nav-user { display: flex; align-items: center; gap: var(--ws-gap-sm) 0.75rem; flex-wrap: wrap; }
.admin-nav-email { font-size: 0.85rem; }

/* The accrual profit block (0120). Quieter than the cash table above it, and
   headed so the two can never be read as one figure in two rows. */
.fin-accrual { margin-top: var(--ws-gap-lg); padding-top: var(--ws-gap); border-top: 1px solid var(--border); }
.fin-accrual-h { margin: 0 0 var(--ws-gap-sm); font-size: 1rem; }
.fin-accrual-note { margin: var(--ws-gap-sm) 0 0; font-size: 0.85rem; }

/* --- THE DASHBOARD (0120) ----------------------------------------------------

   Six sections, one column on a phone, two from 64rem. The old screen opened
   with a masthead, a heat line and five money plates — about 300px of reference
   before the first thing Adam could act on. This one opens with the four
   buttons he presses.

   ⚠️ NO PLATES, NO RIVETS, NO ANVIL. The forge vocabulary stays in the
   stylesheet for nothing that uses it any more; it is not deleted here because
   .forge-* rules are shared with other surfaces and a stylesheet sweep is its
   own build. What changed is that this page stopped reaching for them. */
.dash-section { margin: 0 0 var(--ws-gap-lg); }
.dash-h {
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.06em; font-size: 0.85rem; color: var(--steel);
  margin: 0 0 var(--ws-gap-sm);
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--ws-gap-sm);
}
.dash-h-link { font-size: 0.8rem; font-weight: 600; color: var(--copper); text-decoration: none; }
.dash-h-link:hover { color: var(--copper-bright); }

/* THE TOP ROW (0140): + Add, Go to, and a small tasks widget, where a
   four-button grid and a one-number card used to take ~400px. A phone gets
   the two buttons side by side and the tasks as one line under them; from
   46rem it is one row, the tasks widget taking what the buttons leave. */
.dash-top { display: flex; flex-wrap: wrap; gap: var(--ws-gap-sm); align-items: stretch; margin-bottom: var(--ws-gap); }
.dash-top-actions { display: flex; gap: var(--ws-gap-sm); flex: 1 1 100%; }
.dash-pop { position: relative; flex: 1 1 0; min-width: 0; }
.admin .dash-pop-btn {
  display: flex; align-items: center; justify-content: center; gap: 0.4rem;
  min-height: var(--ws-tap); padding: 0.55rem 1rem; list-style: none; cursor: pointer; user-select: none;
}
.dash-pop-btn::-webkit-details-marker { display: none; }
.dash-pop[open] > .dash-pop-btn { outline: 2px solid var(--copper-bright); outline-offset: 1px; }
/* The popout: small and anchored to its button, not a sheet. */
.dash-pop-menu {
  position: absolute; top: calc(100% + 0.35rem); left: 0; z-index: 40;
  width: max-content; min-width: 12rem; max-width: calc(100vw - 2rem);
  max-height: 70vh; overflow: auto;
  display: grid; gap: 2px; padding: 0.35rem;
  background: var(--panel-2); border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
}
.dash-pop--go .dash-pop-menu { left: auto; right: 0; }
.admin .dash-pop-item {
  display: flex; align-items: center; min-height: var(--ws-tap); padding: 0.4rem 0.8rem;
  background: none; border: 0; border-radius: 3px; color: var(--text); text-decoration: none; text-align: left;
  font-family: var(--font-body); font-size: 0.95rem; font-weight: 500; text-transform: none; letter-spacing: normal;
  cursor: pointer; white-space: nowrap;
}
.admin .dash-pop-item:hover, .admin .dash-pop-item:focus-visible { background: var(--panel); color: var(--copper-bright); }
.dash-go-h {
  margin: 0.4rem 0.8rem 0.1rem; font-family: var(--font-display); font-size: 0.72rem;
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted);
}
.dash-go-group { display: grid; align-content: start; }
/* 0142 — the one Add item that leaves the page: under a rule, in the link
   colour, never dressed as the dialog buttons above it. */
.dash-pop-sep { border: 0; border-top: 1px solid var(--border); margin: 0.3rem 0.4rem; }
.admin .dash-pop-link { color: var(--copper); font-weight: 600; }
.admin .dash-pop-link:hover, .admin .dash-pop-link:focus-visible { color: var(--copper-bright); text-decoration: underline; }

/* The tasks widget: the new-task count on one line (the tile it replaced:
   same rule, same link, same words at zero) and, from 46rem, the newest open
   tasks by title. */
.dash-tasks {
  flex: 1 1 100%; min-width: 0; display: flex; align-items: center; gap: 0.75rem;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
}
.admin .dash-tile {
  display: flex; align-items: baseline; gap: 0.45rem; flex: none;
  min-height: var(--ws-tap); padding: 0.45rem 0.8rem; box-sizing: border-box;
  text-decoration: none; color: var(--text); border-left: 4px solid var(--border-strong);
  border-radius: var(--radius) 0 0 var(--radius); align-self: stretch; align-items: center;
}
.admin .dash-tile:hover .dash-tile-what { color: var(--copper-bright); }
.admin .dash-tile.is-live { border-left-color: var(--copper); }
.dash-tile-n { font-family: var(--font-display); font-size: 1.35rem; font-weight: 700; line-height: 1; }
.dash-tile.is-live .dash-tile-n { color: var(--copper); }
.dash-tile-what { font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.85rem; }
.dash-tile-sub { font-size: 0.8rem; color: var(--muted); }
.dash-tasks-peek { display: none; }
@media (min-width: 46rem) {
  .dash-top { flex-wrap: nowrap; }
  .dash-top-actions { flex: none; }
  .dash-pop { flex: none; }
  .dash-pop--go .dash-pop-menu {
    left: 0; right: auto; grid-template-columns: repeat(3, max-content); column-gap: 0.5rem;
    align-items: start; row-gap: 0.4rem;
  }
  .dash-tasks { flex: 1 1 auto; }
  .dash-tasks-peek {
    display: flex; gap: 0 1rem; min-width: 0; margin: 0; padding: 0 0.8rem 0 0; list-style: none;
    font-size: 0.85rem; color: var(--steel); overflow: hidden;
  }
  .dash-tasks-peek li { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .dash-tasks-peek li + li::before { content: "·"; margin-right: 1rem; color: var(--muted); }
}

/* THE MONTH. Two figures and a quiet aside; each row hides itself when the
   gateway did not send its number (see the module's `cents` helper). */
.dash-month-rows { display: block; }
.dash-money-row {
  display: flex; align-items: baseline; gap: var(--ws-gap-sm);
  padding: 0.5rem 0; border-bottom: 1px solid var(--border);
}
.dash-money-row:last-of-type { border-bottom: 0; }
.dash-money-k {
  flex: 0 0 5.5rem; font-family: var(--font-display); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.85rem;
}
.dash-money-v { font-family: var(--font-display); font-size: 1.35rem; font-weight: 700; color: var(--copper-bright); }
.dash-money-note { font-size: 0.8rem; min-width: 0; }
.dash-month-aside { margin: 0.5rem 0 0; font-size: 0.85rem; }

/* NEEDS ATTENTION. A row only exists when its count is non-zero, so every row
   on screen is something to do; when there are none the panel says so once. */
.dash-att-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--ws-gap-sm);
  min-height: var(--ws-tap); padding: 0.6rem 0.8rem;
  background: var(--panel); border: 1px solid var(--border);
  border-left: 4px solid var(--copper);
  border-radius: var(--radius); margin-bottom: var(--ws-gap-xs);
  color: var(--text); text-decoration: none;
}
.dash-att-row:hover { border-color: var(--border-strong); border-left-color: var(--copper-bright); }
.dash-att-what { min-width: 0; overflow-wrap: anywhere; }
.dash-att-go { flex: none; color: var(--steel); }
.dash-att-clear { margin: 0; }

/* TODAY & TOMORROW (0129). Two day groups, each entry a whole-row button that
   opens the appointment sheet in place. Every line wraps rather than widening
   the column: a client name or a street can be any length, and the body never
   scrolls sideways (plan §4). */
.dash-appt-day + .dash-appt-day { margin-top: var(--ws-gap); }
.dash-appt-dayh {
  margin: 0 0 var(--ws-gap-xs); font-size: 0.95rem; font-weight: 700;
  display: flex; align-items: baseline; gap: 0.5rem;
}
.dash-appt-date { font-weight: 400; font-size: 0.85rem; color: var(--muted); }
.dash-appt-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--ws-gap-xs); }
.admin .dash-appt {
  display: flex; flex-direction: column; align-items: stretch; gap: 0.15rem;
  width: 100%; min-width: 0; min-height: var(--ws-tap); padding: 0.6rem 0.8rem;
  background: var(--panel); border: 1px solid var(--border);
  border-left: 4px solid var(--steel);
  border-radius: var(--radius); color: var(--text); text-align: left;
  /* ⚠️ .admin button is display type, uppercase, copper — right for a verb,
     wrong for a client's name and a street. This is a row that happens to be
     a button, so it reads like the attention rows beside it. */
  font-family: var(--font-body); font-weight: 400; font-size: 1rem;
  text-transform: none; letter-spacing: normal; cursor: pointer;
  text-decoration: none;
}
.admin .dash-appt:hover { border-color: var(--border-strong); }
.dash-appt-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--ws-gap-sm); min-width: 0; }
.dash-appt-who { font-weight: 700; min-width: 0; overflow-wrap: anywhere; }
.dash-appt-when { flex: none; font-size: 0.8rem; color: var(--muted); }
.dash-appt-what, .dash-appt-where { min-width: 0; overflow-wrap: anywhere; font-size: 0.9rem; }
.dash-appt-where { color: var(--muted); }
.dash-appt-kind { font-size: 0.75rem; color: var(--copper); text-transform: uppercase; letter-spacing: 0.04em; }
.dash-appt-empty, .dash-appt-none { margin: 0; }

/* The sheet. Sections stack; the facts fold to one column on a narrow phone. */
/* align-content: start — the body stretches to the sheet's height, and a grid
   left to stretch spreads its three sections down the screen with holes between
   them. */
.dash-appt-sheet { display: grid; gap: 1rem; align-content: start; }
.dash-appt-sec { display: grid; gap: 0.4rem; min-width: 0; }
.dash-appt-sec + .dash-appt-sec { border-top: 1px solid var(--border); padding-top: 0.9rem; }
.dash-appt-h {
  margin: 0; font-family: var(--font-display); font-size: 0.82rem;
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted);
}
.dash-appt-client { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; font-size: 1.05rem; overflow-wrap: anywhere; }
.dash-appt-sub, .dash-appt-span { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
.dash-appt-facts {
  margin: 0; display: grid; gap: 0.35rem 1rem;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
}
.dash-appt-facts div { min-width: 0; }
.dash-appt-facts dt { font-size: 0.78rem; color: var(--muted); }
.dash-appt-facts dd { margin: 0; overflow-wrap: anywhere; }
.dash-appt-noweekends {
  margin: 0; padding: 0.5rem 0.7rem; border: 1px solid var(--copper);
  border-radius: var(--radius); font-size: 0.9rem;
}
.dash-appt-addr { margin: 0; overflow-wrap: anywhere; }
/* Escaped before it arrives; pre-wrap keeps the lines somebody typed. */
.dash-appt-notes {
  margin: 0; white-space: pre-wrap; overflow-wrap: anywhere;
  padding: 0.6rem 0.75rem; background: var(--panel);
  border: 1px solid var(--border); border-radius: var(--radius);
  font-size: 0.95rem; line-height: 1.45;
}
.dash-appt-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem; }
/* A link to a record, sized as a target: the one way off the sheet. */
.dash-appt-record {
  display: inline-flex; align-items: center; min-height: var(--ws-tap);
  color: var(--copper); font-weight: 600;
}
.admin .dash-appt-foot .btn { min-height: var(--ws-tap); }
/* 0139 — "Go to…" can be two links (the job and its estimate). */
#appt-sheet-link { display: flex; flex-wrap: wrap; gap: 0 1rem; min-width: 0; }

/* THE DASHBOARD CALENDAR (0139). Our own schedule: crew work, service calls
   and bins, one colour each. Phone first: the week is seven stacked days, the
   month grid's entries shrink to coloured marks with the chosen day listed
   under it. From 46rem the week becomes seven columns and month cells carry
   named chips. */
:root { --cal-crew: #C4632C; --cal-call: #5E93D1; --cal-bin: #5AA469; }
.cal { display: grid; gap: var(--ws-gap-sm, 0.6rem); min-width: 0; }
.cal-k-crew { --k: var(--cal-crew); }
.cal-k-call { --k: var(--cal-call); }
.cal-k-bin { --k: var(--cal-bin); }

.cal-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem; }
.cal-views, .cal-nav { display: flex; gap: 0.25rem; }
.admin .cal-view, .admin .cal-arrow, .admin .cal-todaybtn, .admin .cal-filter {
  min-height: 2.75rem; padding: 0.35rem 0.75rem; border-radius: var(--radius);
  border: 1px solid var(--border-strong); background: var(--panel); color: var(--text);
  font-family: var(--font-body); font-size: 0.9rem; font-weight: 600;
  text-transform: none; letter-spacing: normal; cursor: pointer;
}
.admin .cal-arrow { min-width: 2.75rem; font-size: 1.2rem; line-height: 1; }
.admin .cal-view[aria-pressed="true"] { background: var(--copper); border-color: var(--copper); color: #fff; }
.cal-title { margin: 0; font-weight: 700; flex: 1 1 100%; }
/* 0140 — on a phone the views and the arrows share ONE row (they were two):
   tighter side padding, the arrows pushed right. Every target stays ≥44px. */
@media (max-width: 45.99rem) {
  .admin .cal-view, .admin .cal-todaybtn { padding-left: 0.55rem; padding-right: 0.55rem; }
  .cal-nav { margin-left: auto; }
}
.cal-filters { display: flex; flex-wrap: wrap; gap: 0.4rem; flex: 1 1 100%; }
.admin .cal-filter { display: inline-flex; align-items: center; gap: 0.45rem; }
.cal-swatch { width: 0.8rem; height: 0.8rem; border-radius: 2px; background: var(--k); flex: none; }
/* Hidden kinds read as OFF, not as a colour: struck and dimmed, swatch hollow. */
.admin .cal-filter[aria-pressed="false"] { opacity: 0.55; text-decoration: line-through; }
.cal-filter[aria-pressed="false"] .cal-swatch { background: transparent; border: 2px solid var(--k); }
.cal-empty, .cal-warn { margin: 0; }

/* Entries as rows: the day view, the week, a month's chosen day. The kind is
   the left edge's colour AND a word, so it never rests on colour alone. */
.admin .cal-entry { border-left-color: var(--k); }
.cal-kind { font-size: 0.75rem; color: var(--muted); white-space: nowrap; }
.cal-entry.is-done { opacity: 0.6; }
.cal-entry.is-done .dash-appt-what { text-decoration: line-through; text-decoration-color: var(--muted); }

.cal-day { min-width: 0; padding: 0; }
.cal-dayh { margin: 0 0 0.3rem; font-size: 0.9rem; font-weight: 700; display: flex; gap: 0.5rem; align-items: baseline; }
.cal-today { font-size: 0.72rem; color: var(--copper-bright); text-transform: uppercase; letter-spacing: 0.05em; }
.cal-none { margin: 0; font-size: 0.85rem; }
.cal-week { display: grid; gap: 0.8rem; }
.cal-week .cal-day.is-today .cal-dayh { color: var(--copper-bright); }

/* The month grid. Seven columns at every width; on a phone a cell is its day
   number and up to three coloured marks, and the whole cell is the tap. */
.cal-month { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 2px; }
.cal-mhead { font-size: 0.7rem; color: var(--muted); text-align: center; padding: 0.2rem 0; }
.cal-cell {
  position: relative; min-width: 0; min-height: 3rem; padding: 0.2rem;
  background: var(--panel); border: 1px solid var(--border); border-radius: 3px;
  display: flex; flex-direction: column; gap: 2px;
}
.cal-cell.is-out { opacity: 0.45; }
.cal-cell.is-today { border-color: var(--copper); }
.cal-cell.is-sel { outline: 2px solid var(--copper-bright); outline-offset: -1px; }
.admin .cal-daynum {
  position: absolute; inset: 0; width: 100%; height: 100%; padding: 0.2rem 0.3rem;
  background: none; border: 0; color: var(--text); text-align: left; vertical-align: top;
  display: flex; align-items: flex-start; font-family: var(--font-body);
  font-size: 0.8rem; font-weight: 600; text-transform: none; letter-spacing: normal; cursor: pointer;
}
.cal-chips { margin-top: 1.1rem; display: flex; flex-wrap: wrap; gap: 3px; pointer-events: none; }
.cal-chip {
  display: block; width: 0.55rem; height: 0.55rem; border-radius: 50%;
  background: var(--k); overflow: hidden; text-indent: -999px; white-space: nowrap;
}
.cal-chip.is-done { opacity: 0.45; }
.cal-more { font-size: 0.65rem; color: var(--muted); line-height: 0.55rem; }
.cal-selday { padding-top: 0.3rem; }

@media (min-width: 46rem) {
  .cal-title { flex: 1 1 auto; text-align: right; }
  /* 0140 — from 64rem the filters join the toolbar's row: views, arrows,
     kinds, and the range title at the right. One row instead of two. */
  @media (min-width: 64rem) {
    .cal-filters { flex: none; order: 3; }
    .cal-title { order: 4; }
  }
  /* The week as seven columns. Rows compact to fit a seventh of the width. */
  .cal-week { grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 0.4rem; }
  .cal-week .cal-day { border-top: 2px solid var(--border); padding-top: 0.3rem; }
  .cal-week .cal-day.is-today { border-top-color: var(--copper); }
  .cal-week .dash-appt { padding: 0.35rem 0.45rem; border-left-width: 3px; }
  .cal-week .dash-appt-top { flex-direction: column; gap: 0; }
  .cal-week .dash-appt-who, .cal-week .dash-appt-what { font-size: 0.8rem; }
  .cal-week .dash-appt-when { font-size: 0.7rem; }
  /* Month cells carry named chips, joined across the days of one run. */
  .cal-cell { min-height: 6rem; }
  .admin .cal-daynum { position: static; width: auto; height: auto; padding: 0; min-height: 0; }
  .cal-chips { margin-top: 0; flex-direction: column; flex-wrap: nowrap; pointer-events: auto; }
  .cal-chip {
    width: auto; height: auto; border-radius: 3px; text-indent: 0; padding: 0.1rem 0.3rem;
    font-size: 0.72rem; color: #fff; text-overflow: ellipsis; text-decoration: none;
  }
  .cal-chip.is-cont { border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: -0.3rem; }
  .cal-chip.is-open { border-top-right-radius: 0; border-bottom-right-radius: 0; margin-right: -0.3rem; }
  .cal-more { line-height: 1.2; }
  /* A run's join stops at the grid's edges: the seven weekday headings are
     the first seven children, so Monday cells are 7n+1 and Sunday cells 7n. */
  .cal-cell:nth-child(7n+1) .cal-chip.is-cont { margin-left: 0; border-top-left-radius: 3px; border-bottom-left-radius: 3px; }
  .cal-cell:nth-child(7n) .cal-chip.is-open { margin-right: 0; border-top-right-radius: 3px; border-bottom-right-radius: 3px; }
}

/* SCHEDULED WORK (0137). The groups after today and tomorrow reuse the day
   markup. The closing row and the sold-not-booked rows are attention rows
   (.dash-att-row), because that is what they are: a thing to go and do. */
.dash-closing { margin-bottom: var(--ws-gap); }
.dash-unbooked { margin-top: var(--ws-gap); display: grid; gap: var(--ws-gap-xs); }
.dash-unbooked-h { margin: 0; font-weight: 700; font-size: 0.95rem; }
.dash-unbooked-how { margin: 0 0 0.2rem; font-size: 0.88rem; }
.dash-unbooked-list { display: grid; gap: var(--ws-gap-xs); }
.dash-unbooked-when { display: block; font-size: 0.82rem; color: var(--muted); }
.dash-unbooked-more { margin: 0; font-size: 0.88rem; }
/* The sentence that says where Notes go. Quiet, directly under the heading. */
.dash-appt-hint { margin: 0; font-size: 0.82rem; }
/* The comments section is its own bordered block. The sheet's section rule
   gives the gap, so the log's page-flow margin would only open a hole. */
.dash-appt-comments .act-section { margin: 0; }

/* MARK DONE (0129). The task's title, then one optional note. 16px so iOS does
   not zoom the sheet on focus; the two buttons clear the tap minimum. */
.td-task { margin: 0 0 0.75rem; font-weight: 700; overflow-wrap: anywhere; }
.admin #td-note { width: 100%; font-size: 1rem; min-height: 6rem; }
.td-foot { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
.admin .td-foot .btn { min-height: var(--ws-tap); }

/* TWO COLUMNS FROM 64rem, and the pairing is the argument: quick add is full
   width because it is the first thing; what ARRIVED sits beside what the month
   LOOKS LIKE; what needs you sits beside what happened. 64rem rather than 46rem
   because the attention sentences are long and two narrow columns of wrapped
   text read worse than one wide column of them — the same measured reasoning
   the nav's own breakpoint carries. */
@media (min-width: 64rem) {
  .forge { display: grid; grid-template-columns: 1fr 1fr; gap: 0 var(--ws-gap-lg); align-items: start; }
  .forge > .dash-section:nth-of-type(1),
  .forge > .dash-top { grid-column: 1 / -1; }
  #paused-slot { grid-column: 1 / -1; }
  /* 0139 — the calendar takes the full width. A seven-day week in half of it
     is seven 70px columns, too narrow to read a client's name. */
  .forge > #appts-slot { grid-column: 1 / -1; }
}

/* 0140 — ONE GAP, SAID ONCE. The dashboard's rhythm was three things stacked:
   the page-band padding (gone, see "PAGE BANDS"), each section's own
   margin-bottom, and `.forge { gap: 1.5rem }` further down this file, which
   also beat the 64rem block's intended `gap: 0 …` by coming later. Here the
   grid owns the spacing (id-scoped, so it wins wherever it sits) and a section
   adds none of its own. An empty paused slot takes no row; the banner's fetch
   fills it and it appears. */
#forging-dashboard.forge { display: grid; row-gap: var(--ws-gap); column-gap: var(--ws-gap-lg); }
#forging-dashboard > .dash-section { margin: 0; }
#paused-slot:empty { display: none; }

/* --- THE PHONE'S BOTTOM TAB BAR (0116) --------------------------------------

   Five slots, fixed to the bottom of the screen: the role's four tabs plus
   More. It lives inside #admin-nav so the guard's one reveal covers it (see
   partials/admin-nav.njk for that argument) and position:fixed takes it out of
   the top row's flex layout entirely, so the bar above measures the same with
   it as without.

   ⚠️ display:none IS THE DEFAULT AND phone TURNS IT ON, not the other way
   round. A bar that exists at every width and is hidden by a max-width query
   is one specificity accident away from appearing across the middle of a
   desktop page; a bar that exists only inside its own query cannot.

   ⚠️ THE BOTTOM INSET IS PADDING, NOT MARGIN. On a gesture iPhone the home
   indicator sits over the last ~34px of the screen. Margin would leave a strip
   of page showing under the bar and scrolling behind the indicator; padding
   extends the bar's own background into that strip while keeping the tap
   targets above it.

   EQUAL FIFTHS, and they are equal by construction rather than by arithmetic:
   `flex: 1 1 0` on five visible slots divides the width evenly whatever the
   labels say, which is exactly why four tabs fit here and did not fit the top
   row at 390 (0110 measured that: the labels set the width there, 416px of
   them against 350px available).

   THE CURRENT PAGE IS MARKED THREE WAYS, and only one of them is colour:
   aria-current for anything that reads the page aloud, a weight change, and a
   3px rule across the top of the slot. Colour alone fails the reader it most
   needs to reach. */
.admin-tabbar { display: none; }

@media (max-width: 45.99rem) {
  .admin-tabbar {
    display: flex; align-items: stretch;
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
    background: var(--panel-2);
    /* ⚠️ AN INSET SHADOW, NOT border-top, AND THE REASON IS ONE PIXEL. A 1px
       border makes the bar 57px while --ws-tabbar-h reserves 56, and the site
       footer then ends exactly one pixel underneath it — measured: footer
       bottom 644, bar top 643. The token's whole claim is that it is BOTH the
       bar's height and the space the page keeps clear, and a border quietly
       makes that false. An inset shadow draws the same rule inside the box. */
    box-shadow: inset 0 1px 0 var(--border-strong);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    /* Landscape on a notched phone puts the notch on a side. */
    padding-left: env(safe-area-inset-left, 0px);
    padding-right: env(safe-area-inset-right, 0px);
  }
  /* ⚠️ SCOPED TO .admin SO IT OUTRANKS THE ADMIN'S BLANKET button RULES. The
     More slot is a <button> and `.admin button` (0,1,1) sets a copper
     background, 0.9rem of side padding and a border. A bare `.admin-tab`
     (0,1,0) loses to it, and the bar came out with four 73px links beside a
     97px button — visibly wrong, and it breaks the equal fifths. */
  .admin .admin-tab {
    flex: 1 1 0; min-width: 0;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 0.15rem;
    position: relative;
    min-height: var(--ws-tabbar-h);
    margin: 0; padding: 0.35rem 0.15rem;
    background: none; border: 0; border-radius: 0;
    font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.02em; font-size: 0.62rem; line-height: 1.1;
    color: var(--steel); text-decoration: none; cursor: pointer;
  }
  /* display:flex above beats the UA's [hidden]{display:none}, exactly as
     .admin-nav[hidden] documents. Without this a role's two unused slots stay
     laid out and the bar has six. */
  .admin .admin-tab[hidden] { display: none; }
  .admin-tab-label {
    max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .admin-tab .nav-icon { flex: none; }
  .admin-tab:active { background: var(--panel); }
  /* THE CURRENT PAGE. Weight and a bar, not only a hue. */
  .admin .admin-tab.is-current, .admin .admin-tab[aria-current] {
    color: var(--copper); font-weight: 700;
  }
  .admin-tab.is-current::before, .admin-tab[aria-current]::before {
    content: ""; position: absolute; top: 0; left: 22%; right: 22%; height: 3px;
    background: currentColor; border-radius: 0 0 3px 3px;
  }

  /* ⚠️ THE PAGE RESERVES THE BAR'S SPACE, ON THE BODY, ONCE. The bar is fixed,
     so it covers whatever is under it — the last row of a list, a section's
     Save button. It went on <body> when the public site footer still rendered
     outside .admin (dropped from admin pages in 0141); it stays there because
     the body is the one element every admin page's end is inside. Driven by
     the token rather than by a number so a taller bar cannot silently stop
     being cleared. base.njk puts .admin-app on the body of /admin/* only. */
  body.admin-app {
    padding-bottom: calc(var(--ws-tabbar-h) + env(safe-area-inset-bottom, 0px));
  }
}

/* ⚠️ THE BAR GETS OUT OF THE WAY OF A SHEET, and this is not decoration. Below
   46rem a ws-modal is a FULL-SCREEN sheet with its footer pinned to the bottom
   of the visible viewport — which is exactly where the tab bar is. Left up, the
   bar sits over the sheet's Save button, or under the software keyboard with
   the Save button, depending on which won the z-index. It comes back on close
   because `modal-open` is removed on close; there is no second code path to
   forget. Specificity (0,2,1) beats the bar's own display:flex (0,1,0), so this
   holds regardless of source order or media query. */
body.modal-open .admin-tabbar { display: none; }

/* --- THE MORE SHEET (0116) ---------------------------------------------------

   Every tab and every gear item as a large grid button, grouped. Two columns at
   390 — wide enough for "Service Calls" on one line, tall enough that the whole
   cell is the target rather than the word inside it.

   It is a ws-modal, so below 46rem it is already a full-screen sheet with its
   own scrolling body and the safe-area insets paid; nothing here re-states any
   of that. What is here is the grid and the quieter treatment for the gear
   group. */
.more-group {
  margin: 1.25rem 0 0.5rem; padding-bottom: 0.3rem;
  font-family: var(--font-display); font-size: 0.8rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--steel); border-bottom: 1px solid var(--border);
}
.more-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--ws-gap-sm); }
.more-item {
  display: flex; align-items: center; gap: 0.6rem;
  min-height: var(--ws-tap); padding: 0.7rem 0.8rem;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 0.85rem;
  color: var(--text); text-decoration: none;
}
.more-item .nav-icon { flex: none; color: var(--steel); }
.more-item-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.more-item:hover { border-color: var(--border-strong); }
/* Same three signals as the bar: aria-current, weight, and a shape — the copper
   edge — so the mark survives being unable to see the colour. */
.more-item.is-current, .more-item[aria-current] {
  color: var(--copper); font-weight: 700;
  border-color: var(--copper); border-left-width: 4px;
}
.more-item.is-current .nav-icon, .more-item[aria-current] .nav-icon { color: var(--copper); }
.more-grid-quiet .more-item {
  background: none; font-size: 0.78rem; color: var(--steel);
}
/* The sheet's body is the whole of it — there is no form and no footer — so it
   gets the bottom inset the ws-modal foot would otherwise have paid. */
.more-sheet .ws-modal-body { padding-bottom: max(1.25rem, env(safe-area-inset-bottom, 0px)); }

/* --- THE SHARED PAGE LAYOUT (0113) -----------------------------------------

   Five primitives, replacing four header shapes, seven hand-built filter bars
   and twenty-four per-page action containers — the 0108 recon's count. Only
   what Clients and Leads both need; a primitive without a consumer is a guess
   with a nice name on it.

   Everything here is phone-first and uses the 0110 tokens, so the tap minimum
   and the input size are stated once and inherited rather than repeated. */

/* Visually hidden but read aloud. A search box with a placeholder and no label
   is a field a screen reader announces as "edit text". */
.ws-visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* --- ws-page-head --- */
.ws-page-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--ws-gap-sm) var(--ws-gap); flex-wrap: wrap;
  margin: 0 0 var(--ws-gap);
}
.ws-page-head-text { min-width: 0; }
.ws-page-title { margin: 0; }
.ws-page-summary { margin: 0.2rem 0 0; font-size: 0.92rem; }
/* Full width on a phone: a primary action squeezed beside a title is a 96px
   button, and the whole point of the head is that the one thing you came to do
   is unmissable. */
.ws-page-primary { flex: none; }
@media (max-width: 45.99rem) {
  .ws-page-primary { width: 100%; }
}

/* --- ws-filterbar --- */
.ws-filterbar {
  display: flex; align-items: center; gap: var(--ws-gap-sm);
  margin: 0 0 var(--ws-gap); min-width: 0;
}
.ws-filterbar-search { flex: 1 1 12rem; min-width: 0; margin: 0; }
.admin .ws-search {
  width: 100%; min-height: var(--ws-tap);
  padding: 0.5rem 0.7rem;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
}
/* ⚠️ THE CHIPS SCROLL, THE BODY DOES NOT. At 390 four chips and a search box do
   not fit a row. Wrapping makes the bar taller on every page — the thing the
   nav's 246px budget was fought over — so they scroll inside a box with its own
   overflow-x. `min-width: 0` is what lets this flex child shrink at all, and
   without it the whole bar pushes the page sideways (the 0083 family). */
.ws-chips {
  display: flex; gap: var(--ws-gap-xs); flex: 0 1 auto;
  min-width: 0; overflow-x: auto; scrollbar-width: thin;
  /* No wrap: a chip row that wraps is a second row of chrome. */
  flex-wrap: nowrap;
  -webkit-overflow-scrolling: touch;
}
.admin .ws-chip {
  flex: none; min-height: var(--ws-tap);
  display: inline-flex; align-items: center; gap: 0.35rem;
  padding: 0.4rem 0.75rem; white-space: nowrap;
  font-size: 0.85rem; font-family: var(--font-display); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.03em;
  color: var(--steel); background: transparent;
  border: 1px solid var(--border-strong); border-radius: var(--radius);
}
.admin .ws-chip.is-active { color: #14110E; background: var(--copper); border-color: var(--copper); }
.ws-chip-count { opacity: 0.75; font-weight: 400; }
/* A toggle beside the tabs (ec-leads): it never shrinks into its own scroll
   box — the tabs' row is the one that scrolls, so the toggle stays whole. */
.ws-chip-toggles { flex: none; overflow: visible; }

/* --- ws-list --- */
.ws-list { list-style: none; margin: 0; padding: 0; }
.ws-row { display: flex; align-items: stretch; gap: var(--ws-gap-xs); }
.ws-row + .ws-row { margin-top: var(--ws-gap-xs); }
/* ⚠️ THE WHOLE ROW IS THE TARGET. On a phone a 44px strip with a tappable name
   somewhere in the middle is a coin toss. text-align:left because a <button>
   centres by default and these are records, not labels. */
.admin .ws-row-open {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; gap: 0.15rem;
  min-height: var(--ws-tap); padding: 0.6rem 0.75rem;
  text-align: left; font: inherit; text-transform: none; letter-spacing: 0;
  color: var(--text); background: var(--panel);
  border: 1px solid var(--border); border-radius: var(--radius);
}
.admin .ws-row-open:hover { border-color: var(--border-strong); }
/* Focus is visible for a keyboard, and NOT hover-only: the plan's §4 rule. */
.admin .ws-row-open:focus-visible { outline: 2px solid var(--copper); outline-offset: 2px; }
.ws-row-title { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; font-weight: 600; }
.ws-row-meta { color: var(--muted); font-size: 0.88rem; min-width: 0; }
/* Markup a caller supplies — a money strip, a chip row. Its own line, and it
   keeps whatever styling its own prefix already gives it. */
.ws-row-extra { display: block; min-width: 0; font-size: 0.88rem; }
/* Denser from tablet up: the meta lines sit beside the title instead of under
   it, so more rows fit a screen that has room for them. */
@media (min-width: 46rem) {
  .admin .ws-row-open { flex-direction: row; align-items: baseline; gap: var(--ws-gap); }
  .ws-row-title { flex: 0 0 auto; }
  .ws-row-meta { flex: 1 1 auto; }
}

/* A row with a thumbnail (0132b, Projects): the image on the left, spanning the
   text lines, at every width. A grid, so the image never pushes the title off
   a 390px row and the meta lines stay under the title rather than beside it. */
.admin .ws-row.has-thumb .ws-row-open {
  display: grid; grid-template-columns: 4.5rem minmax(0, 1fr);
  column-gap: var(--ws-gap-sm, 0.6rem); row-gap: 0.15rem; align-items: center;
}
.ws-row.has-thumb .ws-row-thumb {
  grid-row: 1 / span 4; width: 4.5rem; aspect-ratio: 4 / 3;
  border-radius: calc(var(--radius) - 2px); overflow: hidden; background: var(--panel-2);
}
.ws-row.has-thumb .ws-row-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ws-row.has-thumb .ws-row-title,
.ws-row.has-thumb .ws-row-meta,
.ws-row.has-thumb .ws-row-extra { grid-column: 2; }

/* --- ws-overflow --- */
.ws-overflow { position: relative; flex: none; }
.admin .ws-overflow-btn {
  list-style: none; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--ws-tap); min-width: var(--ws-tap); min-height: var(--ws-tap); height: 100%;
  font-size: 1.3rem; line-height: 1;
  color: var(--steel); background: var(--panel);
  border: 1px solid var(--border); border-radius: var(--radius);
}
.ws-overflow-btn::-webkit-details-marker { display: none; }
.admin .ws-overflow-btn:hover { color: var(--text); border-color: var(--border-strong); }
/* The same trap .admin-nav[hidden] documents: display:flex below beats the UA's
   display:none on a closed <details>'s content, so the closed state has to be
   said explicitly or every menu stays laid out while reading as shut. */
.ws-overflow:not([open]) .ws-overflow-menu { display: none; }
/* Opened upward by wireOverflow when the menu would hang off the bottom — the
   last row in a list, which is exactly the row a thumb reaches first. */
.ws-overflow.is-up .ws-overflow-menu { top: auto; bottom: calc(100% + 0.25rem); }
.ws-overflow-menu {
  display: flex; flex-direction: column;
  position: absolute; z-index: 30; top: calc(100% + 0.25rem);
  /* ⚠️ RIGHT-ANCHORED, which is what keeps it on screen. The ⋯ sits at the
     right edge of a row; a menu anchored left would hang off a 390px viewport
     entirely. max-width stops a long label doing the same thing. */
  right: 0; min-width: 11rem; max-width: min(16rem, calc(100vw - 2rem));
  padding: 0.25rem;
  background: var(--panel-2); border: 1px solid var(--border-strong);
  border-radius: var(--radius);
}
.admin .ws-overflow-item {
  min-height: var(--ws-tap); padding: 0.5rem 0.7rem;
  text-align: left; font: inherit; text-transform: none; letter-spacing: 0;
  color: var(--text); background: transparent; border: 0; border-radius: var(--radius);
  white-space: nowrap;
}
.admin .ws-overflow-item:hover { background: var(--panel); }
.admin .ws-overflow-item.is-danger { color: var(--price-down, var(--copper-bright)); }

/* --- ct: the contact chooser (admin-contact.js) ---
   The value is TEXT and stays text: no truncation, no ellipsis, selectable by
   drag (Heather, Windows Firefox). The dotted underline is the only hint it is
   tappable; it must never look like a button you have to press to see it. */
.ct-pick { position: relative; display: inline; }
/* inline-block, so a value that wraps (an address on a phone) is one solid
   rectangle to tap. As plain inline text it is two line boxes with a gap
   between them, and a tap in the gap landed on the paragraph and did nothing
   (found at 390px on the job page). max-width keeps it wrapping, not clipped. */
.admin .ct-val {
  display: inline-block; max-width: 100%;
  cursor: pointer;
  -webkit-user-select: text; user-select: text;
  color: inherit; overflow-wrap: anywhere;
  text-decoration: underline dotted; text-underline-offset: 0.2em;
  text-decoration-color: var(--steel);
  border-radius: 3px;
}
.admin .ct-val:hover { text-decoration-color: var(--copper-bright); }
.admin .ct-val:focus-visible { outline: 2px solid var(--copper-bright); outline-offset: 2px; }
.ct-pick.is-open > .ct-val { text-decoration-color: var(--copper-bright); }
/* Built on open, removed on close (admin-contact.js), so no closed state. */
.ct-menu {
  display: flex; flex-direction: column;
  position: absolute; z-index: 40; top: calc(100% + 0.25rem); left: 0;
  min-width: 9rem; max-width: min(16rem, calc(100vw - 2rem));
  padding: 0.25rem;
  background: var(--panel-2); border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  text-align: left;
}
.ct-pick.is-up > .ct-menu { top: auto; bottom: calc(100% + 0.25rem); }
.ct-pick.is-right > .ct-menu { left: auto; right: 0; }
.admin .ct-item {
  display: flex; align-items: center;
  min-height: var(--ws-tap); padding: 0.5rem 0.7rem;
  font: inherit; font-size: 0.95rem; text-transform: none; letter-spacing: 0; text-decoration: none;
  color: var(--text); background: transparent; border: 0; border-radius: var(--radius);
  white-space: nowrap; cursor: pointer;
}
.admin .ct-item:hover, .admin .ct-item:focus-visible { background: var(--panel); color: var(--copper-bright); }
/* The confirmation sits ON the control, beside the value it copied. Empty and
   out of flow until it has something to say. */
.ct-flash:empty { display: none; }
.ct-flash {
  margin-left: 0.4rem; padding: 0.05rem 0.4rem;
  font-size: 0.8rem; color: var(--text);
  background: var(--panel); border: 1px solid var(--border-strong); border-radius: 999px;
  white-space: nowrap;
}
.ct-who { margin: 0 0 var(--ws-gap-sm, 0.6rem); }
.ct-who p { margin: 0.15rem 0 0; }
.ct-name { font-weight: 600; color: var(--text); }
.ct-reach, .ct-addr { color: var(--steel); font-size: 0.95rem; }
.ct-reach .ct-val, .ct-addr .ct-val, .ct-reach a { color: var(--text); }
.ct-label { color: var(--steel); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; margin-right: 0.25rem; }
/* WO-0063 — the client popup (admin-client-popup.js): contact, the enquiry's
   photos and what they told us, opened in place over an estimate, job or invoice. */
.cp-open-line { margin: 0.25rem 0 var(--ws-gap-sm, 0.6rem); }
.est-client-card .cp-open { margin-top: 0.35rem; }
.cp-acct { margin: 0 0 0.4rem; font-size: 0.9rem; }
.cp-sec { margin: 1rem 0 0; padding-top: 0.8rem; border-top: 1px solid var(--border); }
.cp-h { margin: 0 0 0.45rem; font-size: 1rem; }
.cp-h4 { margin: 0.5rem 0 0.25rem; font-size: 0.85rem; color: var(--steel); text-transform: uppercase; letter-spacing: 0.04em; }
.cp-meta, .cp-none { margin: 0 0 0.35rem; }
/* What a person typed keeps its line breaks, and a long word wraps at 390px. */
.cp-text { margin: 0 0 0.35rem; white-space: pre-wrap; overflow-wrap: anywhere; }
.cp-more { margin: 0.5rem 0 0; }
/* The lead's notes, read-only (WO-0063): the log newest first, date above words. */
.cp-ro .muted { text-transform: none; letter-spacing: 0; font-weight: 400; }
.cp-notes { list-style: none; margin: 0 0 0.35rem; padding: 0; }
.cp-note { display: flex; flex-direction: column; gap: 0.1rem; padding: 0.35rem 0; border-top: 1px dashed var(--border); }
.cp-note:first-child { border-top: 0; padding-top: 0; }
.cp-note-at { font-size: 0.8rem; }
.cp-note .cp-text { margin: 0; }
.cp-other { margin: 0.6rem 0 0; padding-top: 0.6rem; border-top: 1px dashed var(--border); }
.cp-other:first-of-type { margin-top: 0; padding-top: 0; border-top: 0; }
.cp-other-name { margin: 0 0 0.15rem; font-weight: 600; }
/* WO-0063 #1 — the same view, in a panel on the estimate builder. */
.cp-panel-inline { margin: 0.5rem 0 var(--ws-gap-sm, 0.6rem); padding: 0.5rem 0.75rem; border: 1px solid var(--border); border-radius: 8px; }
.cp-inline-summary { cursor: pointer; font-weight: 600; min-height: 44px; display: flex; align-items: center; flex-wrap: wrap; gap: 0.25rem; }
.cp-inline-ro { font-weight: 400; font-size: 0.85rem; }
.cp-inline-body { max-height: 60vh; overflow-y: auto; overscroll-behavior: contain; }
.cp-inline-body > .cp-sec:first-child { margin-top: 0.25rem; }

/* --- ws-detail-head --- */
.ws-detail-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--ws-gap-sm) var(--ws-gap); flex-wrap: wrap;
  margin: 0 0 var(--ws-gap);
}
.ws-detail-back { flex: 0 0 auto; order: -1; }
@media (max-width: 45.99rem) {
  /* The back control gets its own line on a phone: it is the way out, and a way
     out squeezed beside a title is the control people miss. */
  .ws-detail-back { width: 100%; }
}
.ws-detail-title { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; min-width: 0; }
.ws-detail-title h2 { margin: 0; }
.ws-detail-actions { display: flex; align-items: center; gap: var(--ws-gap-xs); flex: none; }

/* A section's own action row, and the slot an inline confirm renders into. */
.ws-section-actions { display: flex; align-items: center; gap: var(--ws-gap-sm); margin-top: var(--ws-gap-sm); flex-wrap: wrap; }
.ws-row-side { display: flex; flex-direction: column; align-items: stretch; gap: var(--ws-gap-xs); flex: none; }
.sc-confirm:empty { display: none; }
/* 0152 — a service call's draft invoice, said above its row: the one thing in
   Charge waiting on somebody. A left rule in the accent, not an alarm. */
.sc-charge-waiting { margin: 0 0 var(--ws-gap-sm); padding: 0.5rem 0.75rem; border-left: 3px solid var(--copper); background: var(--panel); }

/* A section that failed to mount (0112). Small and quiet: it is a report, not
   an alarm, and the page around it is working. It must still be VISIBLE —
   a section that vanished silently is indistinguishable from one that had
   nothing to say, and "Profit" quietly absent from a money screen is the worst
   outcome available. */
.ws-section-dead {
  margin: 0.5rem 0; padding: 0.6rem 0.8rem;
  border: 1px dashed var(--border-strong); border-radius: var(--radius);
  font-size: 0.9rem;
}

/* --- Service calls, the minimal list (0110) — phase 3 gives it the real page. */
.sc-rows { list-style: none; margin: 0; padding: 0; }
.sc-row + .sc-row { margin-top: 0.4rem; }
.sc-row-link {
  display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;
  padding: 0.7rem 0.85rem; min-height: var(--ws-tap);
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); color: var(--text); text-decoration: none;
}
.sc-row-link:hover { border-color: var(--border-strong); }
.sc-row-who { font-weight: 600; flex: 1 1 10rem; min-width: 0; }
.sc-row-total { color: var(--price); font-weight: 600; }
.sc-row-when { font-size: 0.85rem; }
/* The crew editor on a service call's open visit (2026-10-01): who and for
   what pay, saved in place with update_job. */
.sc-crew { flex-basis: 100%; margin-top: 0.6rem; display: grid; gap: 0.4rem; }
.sc-crew-need { margin: 0; color: #F2B8A0; font-weight: 600; }
.sc-crew-fields { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: end; }
.sc-crew-field { display: grid; gap: 0.2rem; font-size: 0.85rem; color: var(--muted); flex: 1 1 10rem; }
.sc-crew-field:has(.sc-crew-pay) { flex: 0 1 8rem; }
.sc-crew-field[hidden] { display: none; } /* 0169 — the "why", until a set figure moves */
.admin .sc-crew select, .admin .sc-crew input, .admin .sc-crew .btn { min-height: var(--ws-tap); }
.sc-crew .set-msg { margin: 0; }
/* The same question in the shared schedule dialog, for a unit with nobody. */
.sf-crew { display: grid; gap: 0.35rem; margin: 0 0 0.9rem; }
.sf-crew[hidden] { display: none; }
.sf-crew-need { margin: 0; color: #F2B8A0; font-weight: 600; }

/* --- Manual (phone) booking form --- */
/* THE BIN BOOKING DIALOG (0142, was the inline .manual-booking section). The
   fields keep their mb-* rules below; the body is the dialog's scroller. */
.mb-body { display: grid; gap: 0.35rem; align-content: start; }
.mb-body > label { margin-top: 0.55rem; }
.mb-body > label:first-child { margin-top: 0; }
.mb-foot { display: grid; gap: 0.5rem; }
.mb-foot .step-error { margin: 0; }
.mb-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.admin .mb-actions .btn { min-height: var(--ws-tap); }
/* ⚠️ THE [hidden] TRAP, CLOSED. The two views and their two button rows are
   switched with the hidden attribute, and `display: grid` / `display: flex`
   above beat the browser's [hidden] rule: the first build of this dialog showed
   the form AND the result, Create AND Done, at once. Said explicitly here. */
.mb-body[hidden], .mb-actions[hidden] { display: none; }
/* The result: what was booked, first thing on screen, and in link mode the
   link itself with Copy, which is the part that must not be lost. */
.mb-result-head { margin: 0; font-size: 1.1rem; font-weight: 700; color: var(--text); }
.mb-result-sub { margin: 0.2rem 0 0.4rem; }
.mb-link-note { margin: 0.6rem 0 0; font-size: 0.85rem; }
.admin .mb-link-row .btn { min-height: var(--ws-tap); }
.mb-link-row input { min-height: var(--ws-tap); }
.mb-row { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.mb-row #mb-city { flex: 1 1 9rem; }
.mb-row #mb-state { flex: 1 1 4rem; text-transform: uppercase; }
.mb-row #mb-zip { flex: 1 1 5rem; }
#mb-success { margin-top: 0.75rem; color: #9FD8B0; }

.mb-pay { border: 1px solid var(--border); border-radius: var(--radius); padding: 0.75rem 1rem; margin: 1rem 0 0; display: grid; gap: 0.5rem; }
.mb-pay legend { padding: 0 0.4rem; font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.8rem; color: var(--steel); }
.mb-pay .radio { display: flex; align-items: center; gap: 0.6rem; margin: 0; font-family: var(--font-body); font-weight: 400; text-transform: none; letter-spacing: 0; font-size: 1rem; color: var(--text); }
.mb-pay .radio input { width: 1.2rem; height: 1.2rem; accent-color: var(--copper); flex: none; }
.mb-total-row { margin: 1rem 0 0; font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); }
.mb-total-row #mb-total { color: var(--price); font-size: 1.25rem; margin-left: 0.4rem; }
.mb-link-panel { margin-top: 1rem; padding: 0.9rem 1rem; background: var(--panel-2); border: 1px solid var(--copper); border-left-width: 3px; border-radius: var(--radius); }
.mb-link-head { margin: 0 0 0.6rem; color: var(--text); }
.mb-link-row { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.mb-link-row input { flex: 1 1 12rem; min-width: 0; font-size: 0.85rem; }
.mb-link-row .btn { flex: none; }

/* --- Awaiting-payment holds --- */
.await-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); gap: 0.75rem; }
.await-card { background: var(--panel); border: 1px solid var(--border); border-left: 3px solid var(--copper); border-radius: var(--radius); padding: 0.85rem; }
.await-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.await-head strong { color: var(--text); }
.await-meta { color: var(--muted); font-size: 0.9rem; margin-top: 0.25rem; }
.await-count { color: var(--steel); font-size: 0.9rem; margin-top: 0.25rem; }
.await-count [data-countdown] { font-family: var(--font-display); font-weight: 600; color: var(--copper-bright); }
.await-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.7rem; }
.admin .await-actions button { padding: 0.35rem 0.6rem; min-height: 2.25rem; font-size: 0.8rem; }

/* --- Payment-status badges --- */
.badge { display: inline-block; font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.7rem; padding: 0.15rem 0.5rem; border-radius: 999px; border: 1px solid var(--border-strong); color: var(--muted); white-space: nowrap; }
.badge-paid { color: #9FD8B0; border-color: #3B5E45; }
.badge-wait { color: var(--copper-bright); border-color: var(--copper); }
.badge-unpaid { color: #E7C98A; border-color: #6E5B34; }
.badge-cancel { color: var(--muted); border-color: var(--border-strong); }
.bc-foot { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }

.step-error.ok { color: #9FD8B0; }

.inventory-list { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 0.5rem; }
.inventory-list li { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 0.75rem 0.85rem; color: var(--muted); }
.inventory-list li strong { color: var(--text); }
.inv-out { color: var(--text); font-weight: 600; }
.inv-edit { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem; flex-wrap: wrap; }
.inv-label {
  margin: 0; font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 0.75rem; color: var(--muted);
}
.admin .inv-units { width: 5rem; min-height: 2.5rem; padding: 0.35rem 0.5rem; }
.admin .inv-save { padding: 0.45rem 0.8rem; min-height: 2.5rem; }
.inv-msg { display: block; margin-top: 0.4rem; color: #F2B8A0; font-size: 0.85rem; }
.inv-msg.ok { color: #9FD8B0; }

/* --- Settings: pricing tiers + bin config --- */
.admin-note { color: var(--muted); font-size: 0.9rem; margin: -0.4rem 0 0.9rem; }
.settings { display: grid; gap: 1.5rem; max-width: 48rem; }
.set-block { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.1rem; }
.set-h { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.85rem; color: var(--steel); margin: 0 0 0.85rem; }
.set-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.75rem; }
.set-item { padding-bottom: 0.75rem; border-bottom: 1px solid var(--border); }
.set-item:last-child { border-bottom: 0; padding-bottom: 0; }
.set-fields { display: flex; align-items: flex-end; gap: 0.75rem; flex-wrap: wrap; }
.set-fields .inv-label,
.set-bin-grid .inv-label { display: flex; flex-direction: column; gap: 0.25rem; }
.admin .set-tier-label { width: 12rem; min-height: 2.5rem; padding: 0.35rem 0.5rem; }
.admin .set-tier-price { width: 7rem; min-height: 2.5rem; padding: 0.35rem 0.5rem; }
.admin .set-tier-save,
.admin .set-bin-save { min-height: 2.5rem; }
.set-bin-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: 0.75rem; margin-bottom: 0.9rem; }
.admin .set-bin-grid input { width: 100%; min-height: 2.5rem; padding: 0.35rem 0.5rem; }
.set-bin-foot { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.set-msg { display: inline-block; margin: 0; font-size: 0.85rem; }
/* `.set-msg`'s inline-block and `.inv-msg`'s block both beat the UA's
   [hidden] { display: none }, so a hidden message span still painted its
   border and padding — a small empty bordered box next to every Save button
   in the admin. Say it explicitly. (Same trap as .admin-nav[hidden].) */
.set-msg[hidden], .inv-msg[hidden], .step-error[hidden] { display: none; }

/* --- Services routing panel (dumpster/junk → book online, not a quote) --- */
.route-panel { margin: 0.5rem 0 0; padding: 1.1rem 1.25rem; background: var(--panel); border: 1px solid var(--copper); border-left: 3px solid var(--copper); border-radius: var(--radius); }
.route-panel .route-lead { margin: 0 0 0.9rem; color: var(--text); }
.route-panel .route-actions { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center; }
.route-panel .route-call { margin: 0.8rem 0 0; font-size: 0.9rem; }

/* --- Admin leads --- */
.leads-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem 1rem; flex-wrap: wrap; margin-bottom: 0.85rem; }
.leads-count { color: var(--muted); }
.leads-count strong { color: var(--text); }
.admin .lead-toggle { background: transparent; color: var(--steel); border-color: var(--border-strong); }
.lead-bookable { display: inline-block; margin: 0.15rem 0 0.35rem; }
.lead-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.75rem; }
.lead-item {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.85rem 0.9rem;
}
.lead-head {
  display: flex; flex-wrap: wrap; gap: 0.15rem 0.75rem;
  align-items: baseline; justify-content: space-between;
}
.lead-head strong { color: var(--text); }
/* ⚠️ min-width: 0 AND overflow-wrap, THE 0083 FAMILY. A flex item defaults to
   min-width:auto, which means it cannot shrink below its own content — so a
   long service type in a `space-between` row is one unbreakable token away from
   running past the card's right edge instead of wrapping under the name.
   Measured at 390/768/1024/1280 against every service type that exists in the
   data (longest: 25 characters) and against a hostile 90-character token, and
   it did NOT overflow — see the report. This is the structural guarantee that
   it cannot, written the way `.peek-meta` twenty lines below already has it. */
.lead-meta { color: var(--muted); font-size: 0.85rem; min-width: 0; overflow-wrap: anywhere; }
.lead-contact { color: var(--steel); font-size: 0.9rem; margin-top: 0.15rem; }
/* 0065: the address, on its own line under the contact details. It moved off
   the meta run because a street address wraps that three-part line on a phone;
   it reads at contact weight because that is what it is — the other thing you
   need before you drive out. */
.lead-address { color: var(--steel); font-size: 0.9rem; margin: 0.15rem 0 0; }
/* 0101: where a converted lead went. Directly under the head, above the contact
   details, because on a lead that is already a client "who they became" outranks
   "how to reach them" — the client page holds the current number. */
.lead-converted { font-size: 0.9rem; margin: 0.3rem 0 0; color: var(--muted); }
.lead-converted a { color: var(--copper-bright); }
/* How they heard about us: the same quiet line as the converted link above. */
.lead-heard { font-size: 0.9rem; margin: 0.3rem 0 0; color: var(--muted); }
/* The address group on the add/edit lead forms. Full width, outside the
   two-column grid the short fields sit in. */
.lead-addr-group { margin: 0.5rem 0 0.2rem; }
.lead-desc { color: var(--muted); margin: 0.5rem 0; white-space: pre-wrap; }
/* WO-0072 #51: a referred lead never contacted us. The statement sits under the
   head, ruled off like a note, so it is read before the phone number is. The
   referrer's note below carries the referrer's name as its heading. */
.lead-referral { margin: 0.5rem 0; padding: 0.4rem 0.6rem; border-left: 3px solid var(--copper-bright); font-size: 0.92rem; }
.lead-referral p { margin: 0; }
.lead-referral-note-h { font-size: 0.85rem; margin: 0.5rem 0 0; }
.lead-referral-note { margin-top: 0.15rem; }
.lead-edit { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.35rem; }
.admin .lead-status { width: auto; min-height: 2.5rem; }
.admin .lead-save { min-height: 2.5rem; }
.admin .lead-convert { min-height: 2.5rem; }
.admin .lead-notes { min-height: 3rem; }

/* --- The extra person (0062) -------------------------------------------------
   One tenant or second contact on a lead, and on the client it becomes. Both
   pages use these, which is the point: the contact reads the same before and
   after a conversion.

   THE VOCABULARY IS BORROWED, NOT INVENTED. The link is a .btn-quiet with the
   display font and the uppercasing taken back off, so it reads as the quiet
   link it is rather than a third kind of button; it keeps the border and the
   tap target, so it is still a control on a phone. The kind toggle is
   .inv-filter — the same segmented pill the lead tabs and the invoice filters
   already are — and .is-active on it is that component's own state, not a new
   one. Nothing here defines a colour or a radius of its own. */
.admin button.sc-link {
  min-height: 2.25rem; padding: 0.25rem 0.6rem;
  font-family: var(--font-body); font-weight: 500; font-size: 0.85rem;
  text-transform: none; letter-spacing: 0;
  color: var(--copper);
}
.admin button.sc-link:hover { color: var(--copper-bright); border-color: var(--copper); }
/* Empty on a lead with no contact, so the gap only exists when the link does. */
.lead-contact .sc-link-slot:not(:empty) { margin-left: 0.5rem; }
.lead-sc-check { margin-top: 0.25rem; }
/* Directly under the client's name, tucked up against it — it belongs to the
   heading, not to the fields below. */
.client-sc-line { margin: -0.5rem 0 0.9rem; }

/* The dialog's kind toggle: label on its own line, the two segments beneath. */
.sc-kind { align-items: center; margin-bottom: 0.25rem; }
.sc-kind .sc-kind-label { flex-basis: 100%; margin-bottom: 0.15rem; }

/* --- The peek across the split (4b) -----------------------------------------
   Each tab carries a few of the other side's items. Compact on purpose: a peek
   that grew into a panel would rebuild the filing cabinet the reframe removes.
   Same responsive rules as everything else — minmax(0,1fr) tracks, the
   long-string rule on names, 44px rows. */
.peek { margin: 1.75rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); }
/* The pipeline's own unsigned contracts are not a peek at somewhere else —
   they belong to this tab, so they get no dividing rule above them. */
.peek--live { margin-top: 1.25rem; padding-top: 0; border-top: 0; }
.peek-head {
  display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap;
  margin: 0 0 0.6rem; font-family: var(--font-display); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.8rem; color: var(--steel);
}
.peek-all { margin-left: auto; font-family: var(--font-body); font-size: 0.8rem; text-transform: none; letter-spacing: 0; color: var(--copper); text-decoration: none; }
.peek-all:hover { color: var(--copper-bright); text-decoration: underline; }
.peek-list { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.3rem; }
.peek-item { min-width: 0; }
.admin .peek-open {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 0.3rem 0.8rem; flex-wrap: wrap; width: 100%; min-width: 0; text-align: left;
  min-height: 2.75rem; padding: 0.5rem 0.75rem;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius);
  font-family: var(--font-body); font-weight: 400; text-transform: none; letter-spacing: 0;
}
.admin .peek-open:hover { border-color: var(--steel); }
.peek-what { color: var(--text); min-width: 0; overflow-wrap: anywhere; }
.peek-meta { color: var(--muted); font-size: 0.85rem; min-width: 0; overflow-wrap: anywhere; }
/* 0067 — the one-line subtitle under a section heading. Every stage of the
   funnel now says what it is; this is the sentence that says it. Sits between
   the uppercase heading and the rows, in body type so it reads as prose rather
   than as a second label. */
.peek-sub { margin: -0.3rem 0 0.6rem; font-size: 0.85rem; }

/* 0069 — the change-order panel in amendment context. No checkbox and no
   picker any more: a statement of what this estimate is, then the one decision
   that is still a decision (Add vs Credit). */
.est-co-head { margin: 0 0 0.5rem; font-size: 0.95rem; }
.est-co-target-name { font-size: 0.9rem; }

/* 0067 — THE ROW STACKS ON A PHONE.
   A peek row is name + meta with space-between, which is right at desk width.
   Post-0065 a lead's meta carries a full street address —
   "New · Fence repair · 18925 Rosings Way Triangle, VA 22172 · 11h ago" — and
   at 390px the two share a line, so the address wraps inside a narrow column
   beside the name and reads as rubble. `overflow-wrap: anywhere` above stops it
   overflowing but cannot stop it looking like that.
   Below 34rem the row becomes a column: name on its own line, meta underneath,
   full width, nothing shortened and nothing hidden — the address is the point
   of having it. */
@media (max-width: 34rem) {
  .admin .peek-open {
    flex-direction: column; align-items: stretch; justify-content: flex-start;
    gap: 0.15rem;
  }
}

/* --- Lead site visits (0058) ------------------------------------------------
   Booking an estimate visit with a lead: the add-lead form, the availability
   agenda, and the booked state.

   Responsive by the same rules as the rest of this admin — auto-fit minmax(0,…)
   grids, the long-string rule on anything carrying a name or a summary, 44px
   controls, and 45.99rem as the narrow boundary. Nothing here introduces a
   width of its own.
   =========================================================================== */

/* The add-lead form. auto-fit rather than a fixed column count: five short
   fields go three-up on a desktop and one-up on a phone with no breakpoint. */
.lead-new {
  display: grid; gap: 0.6rem;
  margin: 0 0 1.1rem; padding: 0.9rem 1rem;
  background: var(--panel); border: 1px solid var(--border);
  border-left: 3px solid var(--copper); border-radius: var(--radius);
}
.lead-new-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
  gap: 0.6rem 0.9rem;
}
.lead-new-field { display: grid; gap: 0.2rem; min-width: 0; }
.admin .nl-input { min-height: 2.75rem; width: 100%; }
.admin textarea.nl-input { min-height: 4rem; }
/* The inline edit form. Steel edge rather than the add form's copper: adding is
   the primary act on this page, editing is a correction to something already
   there, and the two shouldn't look like the same button pressed twice. */
.lead-edit-form { border-left-color: var(--steel); margin-top: 0.6rem; }

/* --- Lead photos (0061) -----------------------------------------------------
   The client's own pictures — on the PUBLIC form as previews while they attach,
   and on the admin lead as a strip under the description.

   Both use a wrapping flex of fixed-size squares rather than a grid: the count
   is 0–6 and varies per lead, and a wrap fills the row it has without leaving
   the empty cells a fixed grid would. Same 3.25rem thumbnail the line-photo set
   uses, so the two read as the same thing in two places. */
.lead-photos, .lead-photo-previews {
  display: flex; flex-wrap: wrap; gap: 0.45rem; align-items: center;
  list-style: none; margin: 0.5rem 0 0; padding: 0;
}
/* ⚠️ THE ADMIN STRIP NEEDS A WIDER GAP THAN THE PUBLIC ONE, AND THE NUMBER IS
   MEASURED (0118). Its thumbnails carry a corner × whose tap target is 44px
   wide — 22px either side of a centre that sits about 7px inside the
   thumbnail's right edge. At a 0.45rem (7px) gap the target's right half lands
   on the NEXT thumbnail, which is later in the DOM and wins the hit test: the
   × answered 22px to its left and only 13px to its right, a 36px target
   pretending to be 44. 1rem of gap puts 23px of clear space beside the centre,
   which is the first value that fits. The public form's previews have no × of
   their own inside .admin and keep the tighter gap. */
.admin .lead-photos { gap: 1rem 1rem; }
.lead-photo-thumb {
  display: block; width: 3.25rem; height: 3.25rem; object-fit: cover;
  border-radius: var(--radius); border: 1px solid var(--border);
}
/* The admin thumbnail is a button (it opens the lightbox); strip the chrome so
   it reads as the photo, not as a control wrapped round one. */
.admin .lead-photo-open {
  display: inline-flex; padding: 0; min-height: 0; border: 0; background: none;
  cursor: zoom-in; border-radius: var(--radius);
}
.admin .lead-photo-open:hover .lead-photo-thumb { border-color: var(--copper); }

/* --- The editable strip (0064) ----------------------------------------------
   The 0061 strip gained a × per thumbnail and an add button. Everything above
   is reused as-is; these rules only position the × over its own thumbnail and
   size the two controls.

   .ph-item is what makes the × possible: the thumbnail button and its remove
   button are siblings inside a positioned wrapper, because a button cannot nest
   inside a button — the same constraint the Clients list solved the same way
   for its quick-estimate glyph. */
.ph-item { position: relative; display: inline-flex; line-height: 0; }
/* ⚠️ THE CHROME IS RESET HERE, AND THAT WAS THE OTHER HALF OF THE BUG. This
   rule sized the × and said nothing about its colours, so `.admin button`
   (0,1,1) kept dressing it in copper with a copper border — a small round
   ORANGE blob on top of a photograph. Sizing a button without restating its
   surface is how a control ends up half-styled by a rule three thousand lines
   away. 1.6rem visible (25.6px) with a 44px hit area from .btn-icon. */
.admin .ph-item .lead-photo-remove {
  position: absolute; top: -0.35rem; right: -0.35rem;
  width: 1.6rem; height: 1.6rem; min-width: 0; min-height: 0; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.95rem; line-height: 1; border-radius: 50%;
  background: var(--panel-2); color: var(--text);
  border: 1px solid var(--border-strong);
}
.admin .ph-item .lead-photo-remove:hover {
  color: var(--copper-bright); border-color: var(--copper);
}
/* Matches the thumbnail's own height so the row of controls sits on one line
   whatever it contains. */
.admin .ph-strip .ph-add { min-height: 3.25rem; font-size: 0.85rem; padding: 0.4rem 0.7rem; }
.ph-full { font-size: 0.85rem; }
/* The per-strip message takes the full width so a two-line "3 added, the 4th
   failed" doesn't squeeze the thumbnails it is reporting on. */
.ph-strip .ph-msg { flex-basis: 100%; margin-top: 0.35rem; }
/* On a client the strip is a section of its own, so it needs the top margin the
   lead card's strip gets from the paragraph above it. */
.client-photos-note { margin: -0.35rem 0 0.5rem; font-size: 0.9rem; }
.client-detail .ph-strip { margin-bottom: 1rem; }
.lead-photo-missing { font-size: 0.8rem; }
/* The strip above shares its rule with the PUBLIC form's previews, and that
   rule carries `margin: 0.5rem 0 0` — top only. On the admin lead card the next
   thing is the action row (status, save, edit, schedule, convert, close), which
   has no top margin of its own, so the thumbnails sat flush on top of it.
   Admin-scoped so the public previews keep the spacing they were tuned with:
   `.lead-photos` is the admin strip and `.lead-photo-previews` is the public
   one, so they are already disjoint, and `.admin` says so out loud. */
.admin .lead-photos { margin-bottom: 0.75rem; }

/* PUBLIC form previews. Each carries its own remove control, tucked on the
   corner — the same shape the line-photo editor uses. 1.5rem is small on a
   desktop and still a comfortable thumb target on the phone these are taken on. */
.lead-photo { position: relative; display: inline-flex; }
.lead-photo-remove {
  position: absolute; top: -0.35rem; right: -0.35rem;
  width: 1.5rem; height: 1.5rem; min-height: 0; padding: 0; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1rem; cursor: pointer;
  background: var(--panel-2); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: 999px;
}
.lead-photo-remove:hover { border-color: var(--copper); color: var(--copper-bright); }
.lead-photos-hint { margin: 0.15rem 0 0.4rem; font-size: 0.9rem; }
.lead-photos-msg { margin: 0.4rem 0 0; }
/* THE PHOTO PICKER. A file input cannot be styled, so the input is hidden and a
   label wears the brand's ghost button — .btn + .btn-ghost, not a new button
   style, so it can never drift from the rest of the form.

   Hidden the ACCESSIBLE way (1px + clip), never display:none: the input keeps
   its place in the tab order, so a customer tabbing through the form still
   lands on it and still opens the picker with Enter or Space. The focus ring is
   drawn on the label instead, because the input itself is a clipped pixel and
   its own ring would be invisible. */
.lead-photos-input {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.lead-photos-btn {
  /* The global `label` rule is display:block with its own margin and colour;
     .btn and .btn-ghost win on specificity for the rest, these two do not. */
  margin: 0.1rem 0 0;
  min-height: 2.75rem;
  font-size: 0.95rem;
}
.lead-photos-icon { flex: none; }
.lead-photos-input:focus-visible + .lead-photos-btn {
  outline: 2px solid var(--copper);
  outline-offset: 2px;
}
/* At the six-photo cap services.js sets input.hidden. The visible control is
   the label, so it follows the input's own state rather than being toggled
   separately — one source of truth, and the two cannot disagree. */
.lead-photos-input[hidden] { display: none; }
.lead-photos-input[hidden] + .lead-photos-btn { display: none; }

/* THE BOOKED STATE, above the controls so it reads before them. */
.sv-booked {
  display: flex; align-items: center; gap: 0.4rem 0.7rem; flex-wrap: wrap;
  margin: 0.5rem 0; padding: 0.55rem 0.8rem;
  background: var(--panel-2); border: 1px solid var(--border-strong);
  border-left: 3px solid var(--copper); border-radius: var(--radius);
}
.sv-booked-label {
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.06em; font-size: 0.7rem; color: var(--muted);
}
.sv-booked-when { font-family: var(--font-display); font-weight: 700; color: var(--price); }
.sv-badge { font-size: 0.72rem; }
/* Pushed right when there's room, next in the wrap order when there isn't. */
.admin .sv-cancel { margin-left: auto; min-height: 2.75rem; font-size: 0.85rem; }

.sv-panel {
  display: grid; gap: 0.6rem;
  margin: 0.5rem 0 0.75rem; padding: 0.85rem 0.95rem;
  background: var(--panel); border: 1px solid var(--border-strong);
  border-top: 3px solid var(--copper); border-radius: var(--radius);
}
/* THE GRID ITEMS ARE THE PAIRS, NOT THE FIELDS. Labels and controls used to be
   siblings here, so which label ended up over which control was decided by how
   many columns happened to fit: one column stacked correctly, three or five
   interleaved them, and at the widest the "Length" label sat at the end of one
   row with its select at the start of the next. Each label now travels inside
   an .sv-field with its own control, so no column count can separate them.

   `.sv-pick .inv-label { grid-column: auto }` went with it — the labels are no
   longer grid items of this container, so there is nothing left for it to say.
   It was scoped to .sv-pick, so no other .inv-label is affected. */
.sv-pick {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr));
  gap: 0.6rem 0.9rem;
}
/* Stretched to the tallest field and the control pinned to the bottom, so the
   three controls stay on one line even if a label ever wraps. */
.sv-field {
  display: flex; flex-direction: column; justify-content: flex-end;
  gap: 0.25rem; min-width: 0;
}
.admin .sv-date, .admin .sv-time, .admin .sv-dur { min-height: 2.75rem; width: 100%; }
.admin .sv-notes { min-height: 2.75rem; width: 100%; }
.sv-h {
  margin: 0.2rem 0 0; font-family: var(--font-display); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.7rem; color: var(--copper-bright);
}
.sv-empty { margin: 0; font-size: 0.88rem; }

/* THE AVAILABILITY AGENDA. A list, not a grid — see the note in admin-leads.js.
   Each row is [when][what]; the what wraps and the when never does, so a long
   event title can't push the row wider than the card. */
.sv-agenda { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.3rem; }
.sv-busy {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.6rem; align-items: baseline;
  padding: 0.5rem 0.7rem;
  background: var(--panel-2); border: 1px solid var(--border);
  border-left: 3px solid var(--steel); border-radius: var(--radius);
  font-size: 0.9rem;
}
/* An all-day event blocks the day without pinning an hour — copper edge so it
   reads as a different KIND of busy, not just another slot. */
.sv-busy.is-allday { border-left-color: var(--copper); }
.sv-busy-when { font-family: var(--font-display); font-weight: 700; color: var(--price); white-space: nowrap; }
.sv-busy-what { color: var(--text); min-width: 0; overflow-wrap: anywhere; }

.sv-notify { margin: 0; }
.sv-actions { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.admin .sv-actions .btn { min-height: 2.75rem; }

/* On a phone the time column earns less than the width it takes: the event
   title is what Adam is reading. Same 45.99rem boundary as the rest of the
   admin's narrow rules. */
@media (max-width: 45.99rem) {
  .sv-busy { grid-template-columns: minmax(0, 1fr); row-gap: 0.1rem; }
  .admin .sv-cancel { margin-left: 0; }
}

/* --- Admin clients (Operations) ---
   Tap-and-go: every control is a full-size 44px+ target and inputs stay at the
   16px base so phones never zoom on focus. */
.clients-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 0.75rem 1rem; flex-wrap: wrap; margin-bottom: 0.85rem; }
.client-search-label { display: flex; flex-direction: column; gap: 0.25rem; flex: 1 1 16rem; }
.admin .client-search { min-height: 2.75rem; }
.admin .client-new-btn { flex: none; min-height: 2.75rem; font-size: 0.9rem; padding: 0.5rem 1rem; }

.client-form { display: grid; gap: 0.75rem; max-width: 34rem; margin: 0 0 1.25rem; padding: 1rem 1.1rem; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); }
.client-form .inv-label { display: flex; flex-direction: column; gap: 0.25rem; }
.admin .client-form input, .admin .client-form textarea { font-size: 1rem; min-height: 2.75rem; }
.admin .client-form textarea { min-height: 5rem; }
.client-form-foot { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.admin .client-form-foot .btn { min-height: 2.75rem; font-size: 0.9rem; padding: 0.5rem 1rem; }

.clients-count { color: var(--muted); margin: 0 0 0.6rem; }

/* --- Add-client modal --- */
.modal { position: fixed; inset: 0; z-index: 80; display: flex; align-items: center; justify-content: center; padding: 1rem; }
.modal[hidden] { display: none; }
.modal-backdrop { position: absolute; inset: 0; background: rgba(10, 8, 6, 0.72); }
.modal-panel {
  position: relative; z-index: 1;
  /* A flex item's default `min-width: auto` refuses to shrink below its
     min-content, so one nowrap descendant anywhere inside can push a panel past
     its own max-width. Every modal in the admin is a flex item of `.modal`;
     this line is what makes max-width mean what it says. (0083 followup.) */
  min-width: 0;
  width: 100%; max-width: 32rem; max-height: 88vh; overflow-y: auto;
  padding: 1.1rem 1.25rem 1.25rem;
  background: var(--panel-2); border: 1px solid var(--border-strong);
  border-top: 3px solid var(--copper); border-radius: var(--radius);
}
.modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: 0.9rem; }
.modal-title { margin: 0; font-size: 1.2rem; }
.admin .modal-close {
  flex: none; width: 2.75rem; height: 2.75rem; min-height: 2.75rem; padding: 0;
  font-size: 1.5rem; line-height: 1;
  background: transparent; color: var(--steel); border: 1px solid var(--border-strong);
}
.admin .modal-close:hover { color: var(--text); border-color: var(--steel); }
/* The dialog owns scrolling while it's open. */
body.modal-open { overflow: hidden; }

/* --- ws-modal: THE SHEET/PANEL SHELL (0112) ---------------------------------

   Adam works on a phone and a tablet daily, and a centered 32rem panel on a
   390px screen is a form you scroll the PAGE to fill in — which on iOS means
   the save button is wherever the page happens to have ended up when the
   keyboard closed.

   BELOW 46rem it is a full-screen SHEET; from 46rem up, the centered panel it
   always was. The difference is not decoration: on the sheet the panel owns the
   viewport, the BODY scrolls inside it, and the footer is pinned — so the
   primary action is in the same place every time, keyboard or no keyboard.

   ⚠️ dvh, NOT vh. On a phone browser vh is the LARGEST viewport — the one with
   the URL bar hidden — so `height: 100vh` makes a sheet taller than the screen
   and pushes its own footer under the chrome. dvh tracks what is actually
   visible. Both are written, older first, so a browser without dvh still gets
   a usable sheet rather than nothing.

   ⚠️ SAFE-AREA INSETS, copying the pattern .admin already uses at the top of
   this file. base.njk sets viewport-fit=cover on /admin/* only, which is what
   makes env() resolve to real numbers; without the bottom inset the footer sits
   under the home indicator on a gesture iPhone.

   ⚠️ overflow: hidden ON THE PANEL AND auto ON THE BODY. The panel must not
   scroll — if it does, the header and footer scroll away with it and the whole
   point is lost. `min-height: 0` on the form and the body is what lets a flex
   child actually shrink and hand its overflow to the scroller (the 0083
   family, again). */
.ws-modal-panel { display: flex; flex-direction: column; overflow: hidden; }
.ws-modal-form {
  display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0;
}
/* ⚠️ THE SAME TRAP .admin-nav[hidden] DOCUMENTS, and 0117 walked straight into
   it. display:flex above beats the UA's [hidden]{display:none}, so the close-out
   dialog's three panes — the interview, the settlement, the write-off — were
   all on screen at once, a dialog arguing with itself, with `hidden` set on two
   of them and doing nothing. Say it explicitly. */
.ws-modal-form[hidden] { display: none; }
.ws-modal-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  /* Momentum scrolling inside the sheet on iOS. */
  -webkit-overflow-scrolling: touch;
}
/* Pinned, and opaque: content scrolling underneath must not show through the
   one control the reader is looking for. */
.ws-modal-foot {
  flex: none; position: sticky; bottom: 0; z-index: 1;
  margin: 0; padding-top: 0.75rem;
  padding-bottom: max(0.75rem, env(safe-area-inset-bottom, 0px));
  background: var(--panel-2);
  border-top: 1px solid var(--border);
}

/* PHONE: the sheet. */
@media (max-width: 45.99rem) {
  .ws-modal { padding: 0; align-items: stretch; }
  .ws-modal-panel {
    width: 100%; max-width: none;
    /* Three deep, oldest first, and every line earns its place:
         100vh   anything; wrong on a phone but never zero
         100dvh  tracks the browser's own chrome
         --ws-vvh  the VISIBLE viewport, published by admin-modal.js while the
                   dialog is open — the only one that shrinks for the software
                   keyboard. Without it the save button sits under the keyboard
                   on iOS while looking perfect on every desktop test. */
    height: 100vh; height: 100dvh; height: var(--ws-vvh, 100dvh);
    max-height: 100vh; max-height: 100dvh; max-height: var(--ws-vvh, 100dvh);
    border-radius: 0; border-left: 0; border-right: 0; border-bottom: 0;
    padding: max(1.1rem, env(safe-area-inset-top, 0px)) 1rem 0;
  }
  .ws-modal-foot { padding-left: 0; padding-right: 0; }
}

/* TABLET AND UP: the centered panel, with the same internal scroll split so the
   footer stays put on a long form here too. */
@media (min-width: 46rem) {
  .ws-modal-panel { max-height: 88vh; max-height: 88dvh; }
}
/* A dialog opened ON TOP of another one — the add-an-email popup over the
   schedule/cancel dialog. Higher z-index so it stacks above rather than
   fighting for the same layer; the parent stays open underneath with
   everything still typed into it. Narrower panel so it reads as a small
   detour, not a second full form. */
.modal-stacked { z-index: 90; }
.modal-panel-narrow { max-width: 24rem; }
/* The hint under a notify checkbox: why the box is disabled, and the way out. */
.notify-hint { margin: -0.35rem 0 0.35rem 1.9rem; font-size: 0.86rem; }
.notify-ok { color: var(--copper-bright); }
/* A button that reads as a link — used inline in sentences where the action is
   a detour, not a form control. */
.admin .link-button {
  padding: 0; min-height: 0; border: 0; background: none;
  color: var(--copper); font: inherit; text-decoration: underline; cursor: pointer;
}
.admin .link-button:hover { color: var(--copper-bright); }
/* Cancelling is destructive and the dialog should say so before the buttons. */
.jc-warn { margin: 0 0 0.6rem; color: var(--steel); }

/* --- In-app PDF viewer -----------------------------------------------------
   Full-screen on a phone, a large panel on a desktop. Sits above every dialog
   (a PDF can be opened FROM one), and respects the safe-area insets because in
   standalone mode it covers the notch too. */
.pdfv { position: fixed; inset: 0; z-index: 200; display: flex; }
.pdfv-backdrop { position: absolute; inset: 0; background: rgba(10, 8, 6, 0.9); }
.pdfv-panel {
  position: relative; z-index: 1;
  display: flex; flex-direction: column;
  width: 100%; height: 100%;
  padding-top: env(safe-area-inset-top, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: var(--panel-2);
}
.pdfv-head {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  padding: 0.6rem 0.8rem;
  border-bottom: 1px solid var(--border-strong);
  background: var(--panel);
}
.pdfv-title {
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.02em; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pdfv-actions { display: flex; align-items: center; gap: 0.5rem; flex: none; }
/* ⚠️ SCOPED TO .pdfv, NOT .admin (WO-0008). openPdf appends the overlay to
   <body>, OUTSIDE the .admin wrapper, so every ".admin .pdfv-…" rule here
   never matched: the × was 21px tall on a phone from the day the viewer
   shipped. Measured on the built admin page, fixed by the scope. */
/* The escape hatch. Deliberately a real, always-visible link rather than
   something that appears on failure — an iframe that renders a blank PDF
   reports no error, so there is nothing to detect. */
.pdfv .pdfv-open { min-height: 2.75rem; font-size: 0.85rem; padding: 0.45rem 0.8rem; white-space: nowrap; }
.pdfv .pdfv-close {
  flex: none; width: 2.75rem; height: 2.75rem; min-height: 2.75rem; padding: 0;
  font-size: 1.6rem; line-height: 1;
  background: transparent; color: var(--steel); border: 1px solid var(--border-strong);
}
.pdfv .pdfv-close:hover { color: var(--text); border-color: var(--steel); }
/* On a phone the escape hatch matters more than the title, but "Open in
   browser" eats enough width to truncate the title to nothing. Drop to "Open ↗"
   on narrow screens — the aria-label keeps the full meaning for screen readers. */
@media (max-width: 30rem) {
  .pdfv-open-long { display: none; }
  .pdfv .pdfv-open { padding: 0.45rem 0.6rem; }
}
.pdfv-body { position: relative; flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.pdfv-frame { flex: 1 1 auto; width: 100%; border: 0; background: #fff; }
.pdfv-hint { margin: 0; padding: 0.5rem 0.8rem; font-size: 0.82rem; border-top: 1px solid var(--border); }
/* WO-0008 — every page, drawn by PDF.js: one scrolling column of canvases on
   a neutral grey, so a white page reads as a page. Zoomed, the column scrolls
   both ways (iOS doesn't pinch-zoom inside an element). */
.pdfv-titles { display: flex; align-items: baseline; gap: 0.6rem; min-width: 0; }
.pdfv-count { flex: none; font-size: 0.85rem; color: var(--steel); white-space: nowrap; }
.pdfv-count[hidden], .pdfv-zoom[hidden] { display: none; }
.pdfv .pdfv-zoom { min-height: 2.75rem; font-size: 0.85rem; padding: 0.45rem 0.7rem; }
.pdfv-loading { margin: auto; padding: 2rem; }
.pdfv-pages {
  flex: 1 1 auto; min-height: 0; overflow: auto; -webkit-overflow-scrolling: touch;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 8px; background: #3a3631;
}
.pdfv-pages.is-zoomed { align-items: flex-start; }
.pdfv-page { display: block; flex: none; background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); }

@media (min-width: 48rem) {
  /* Desktop: a large panel rather than edge-to-edge, so the page behind stays
     visible as context. */
  .pdfv { align-items: center; justify-content: center; padding: 2rem; }
  .pdfv-panel {
    width: min(60rem, 100%); height: min(90vh, 100%);
    border: 1px solid var(--border-strong); border-top: 3px solid var(--copper);
    border-radius: var(--radius); overflow: hidden;
  }
}

/* --- The activity log (0046) ----------------------------------------------
   Used in three places with one look: the collapsible section under an
   estimate/contract/job/client, the dashboard's ten-row panel, and the full
   Activity page. Mobile-first — the row stacks its meta line under the summary
   and never needs horizontal room. */
.act-section {
  margin: 1.4rem 0 0;
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--radius);
}
.act-summary-toggle {
  padding: 0.7rem 0.9rem;
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 0.95rem; color: var(--steel);
  cursor: pointer; list-style: revert;
}
.act-summary-toggle:hover { color: var(--text); }
.act-section[open] > .act-summary-toggle { border-bottom: 1px solid var(--border); color: var(--text); }
.act-body-slot { padding: 0.7rem 0.9rem; }

.act-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.55rem; }
.act-row { display: grid; grid-template-columns: 1.6rem 1fr; gap: 0.55rem; align-items: start; }
/* The icon carries the colour, so the row reads at a glance without a legend:
   money, signatures, things sent, things called off. */
.act-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.6rem; height: 1.6rem; border-radius: 999px;
  font-size: 0.8rem; line-height: 1;
  border: 1px solid var(--border-strong); color: var(--steel);
}
/* overflow-wrap because a summary can now be USER text (a note), and a pasted
   URL or a long unbroken string would otherwise push the row wider than its
   container. Automatic summaries are unaffected — they have spaces to break at. */
.act-summary { display: block; line-height: 1.4; overflow-wrap: anywhere; }
.act-meta {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.5rem;
  margin-top: 0.1rem; font-size: 0.82rem; color: var(--muted);
}
.act-when { font-variant-numeric: tabular-nums; }
.act-entity {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em;
  border: 1px solid var(--border-strong); border-radius: 999px; padding: 0.02rem 0.45rem;
}
a.act-entity { color: var(--copper); }
a.act-entity:hover { color: var(--copper-bright); border-color: var(--copper); }

/* THE HIT AREA, NOT THE BOX (0058). Two links in the admin are deliberately
   tiny type — the entity pill on a log row (21px tall) and "See all" beside a
   dashboard heading (13px). Both sit on a baseline inside a line of dense
   metadata, so growing the BOX to 44px would fatten every log row and every
   section heading to make room for the smallest thing on it.

   So the box keeps its size and an invisible ::after carries the target: full
   width of the link, 2.75rem tall, centred on it. Layout is untouched — the
   pseudo-element is absolutely positioned and out of flow — and a thumb
   landing anywhere in that band still lands on the link.

   Measured for the thing that can actually go wrong: the band must not cover
   a NEIGHBOURING control. Log rows are far enough apart that two pills never
   overlap, and nothing sits within 22px above or below "See all". If a row
   ever tightens, this is the rule to re-measure. */
/* .peek-all (4b) joins them: the same shape — a small link riding a heading
   baseline — so it takes the same treatment rather than a second one. */
a.act-entity, a.forge-h-link, a.peek-all { position: relative; }
a.act-entity::after, a.forge-h-link::after, a.peek-all::after {
  content: ""; position: absolute; left: 0; right: 0;
  top: 50%; height: 2.75rem; transform: translateY(-50%);
}
.act-entity-plain { color: var(--muted); }
/* WHO, distinguishable without reading: a client or the system acting on their
   own is the thing Adam scans for. */
.act-by-client { color: var(--copper-bright); }
.act-by-system { color: var(--steel); font-style: italic; }
.act-by-admin { color: var(--muted); }

.act-good .act-icon { color: #9FD8B0; border-color: #9FD8B0; }
/* Money IN — bright, because revenue is what you're looking for in a feed. */
.act-money .act-icon { color: var(--price); border-color: var(--steel); }
/* Money OUT (a contractor payment, an expense) — the same `$` glyph, so the
   three money actions read as one family, but dimmer on both the glyph and the
   ring so a day's costs sit behind its revenue rather than competing with it.
   No new colour: --price vs --steel is a contrast the eye already reads
   elsewhere in this palette. The row's summary says the direction in words too,
   so nothing here depends on colour alone. */
.act-money-out .act-icon { color: var(--steel); border-color: var(--border-strong); }
.act-sent .act-icon,
.act-email .act-icon { color: var(--copper); border-color: var(--copper); }
.act-sched .act-icon { color: var(--copper-bright); border-color: var(--copper); }
.act-bad .act-icon { color: #F2B8A0; border-color: #F2B8A0; }

/* A NOTE reads as a different kind of entry, because it is one: everything else
   in this list is something the system observed, and this is a person talking.
   A panel, a copper edge and the copper glyph — the row is deliberately inset
   from its neighbours rather than aligned with them, which is the whole point:
   it should interrupt the scan, not blend into it. */
.act-note .act-icon { color: var(--copper-bright); border-color: var(--copper); }
.act-row.act-note {
  background: var(--panel); border: 1px solid var(--border);
  border-left: 2px solid var(--copper); border-radius: var(--radius);
  padding: 0.5rem 0.6rem;
}
/* A note is typed in a textarea, so its line breaks are meaningful — a
   paragraph Adam split in two must not collapse into one run of text. */
.act-note .act-summary { white-space: pre-wrap; }

/* The composer, above the history. Full-width field, action row beneath — the
   same shape at 390px as on a desktop, so there is no separate mobile layout to
   keep in step. */
.act-note-form { display: grid; gap: 0.5rem; margin-bottom: 0.8rem; }
.act-note-text {
  width: 100%; min-height: 3.2rem; resize: vertical;
  padding: 0.5rem 0.6rem; font: inherit; line-height: 1.4;
  color: var(--text); background: var(--bg); border: 1px solid var(--border-strong);
  border-radius: var(--radius);
}
.act-note-text:focus-visible { outline: 2px solid var(--copper); outline-offset: 1px; }
.act-note-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem; }
/* A comfortable tap target on a phone, which is where most notes get written. */
.act-note-add { min-height: 2.75rem; }
.act-note-msg { margin: 0; }
/* 0137 — where the words go, under the field (work-unit comments). */
.act-note-hint { margin: -0.2rem 0 0; font-size: 0.82rem; }

/* The client asked for changes (0050) — amber, and deliberately NOT the red
   `declined` sits in. One is work waiting to be done, the other is closed. */
.act-warn .act-icon { color: var(--copper-bright); border-color: var(--copper-bright); }

.act-empty { margin: 0; }
.act-lead { max-width: 44rem; margin-top: -0.4rem; }
.activity-feed .act-list { gap: 0.7rem; }
.act-more { margin-top: 1rem; }
.admin .act-more .btn { min-height: 2.75rem; }
.act-end { margin-top: 1rem; font-size: 0.86rem; }
/* "See all" beside a dashboard panel heading. */
.forge-h-link { font-family: var(--font-body); font-size: 0.8rem; font-weight: 400; margin-left: 0.6rem; letter-spacing: 0; text-transform: none; }

/* --- The schedule dialog's date + duration pair --------------------------- */
/* Two fields, side by side once there's room. A start date and a day count are
   read together — "Monday, five days" — and stacking them on a wide screen
   makes them look like unrelated questions. */
.sf-when { display: grid; gap: 0.6rem; }
@media (min-width: 26rem) { .sf-when { grid-template-columns: 1fr 8rem; align-items: end; } }
.admin .sf-when input { width: 100%; min-height: 2.75rem; }
/* The computed end date, live, before Adam commits to it. */
.sf-preview {
  margin: 0.1rem 0 0.4rem; padding: 0.55rem 0.7rem;
  background: var(--panel); border: 1px solid var(--border);
  border-left: 3px solid var(--copper); border-radius: var(--radius);
  font-size: 0.92rem; min-height: 2.4rem;
}
.sf-preview strong { color: var(--price); }
.sf-snap { color: var(--muted); }

/* --- A job's dates: start, end, NO WEEKENDS (0123) ----------------------- */
/* One block, three forms — see partials/span-fields.njk. Two date pickers side
   by side once there is room; stacked on a phone, where a date input wants the
   full width to show its value without truncating the year. */
.span-fields { display: grid; gap: 0.35rem; }
.span-dates { display: grid; gap: 0.6rem; }
@media (min-width: 26rem) { .span-dates { grid-template-columns: 1fr 1fr; align-items: end; } }
/* ⚠️ 16px, NOT THE .admin input default. iOS zooms the whole page on focus into
   any field under 16px, and on the ws-modal sheet that zoom slides the pinned
   footer off screen. 44px tall for the same reason every tap target is. */
.admin .span-fields input[type="date"] {
  width: 100%; min-height: 2.75rem; font-size: 1rem;
}
.span-noweekends strong { letter-spacing: 0.03em; }
/* The refusal, before submit. Copper, not red: it is an instruction with the
   answer attached, not an error Adam caused. */
.span-warn {
  margin: 0.1rem 0 0; padding: 0.55rem 0.7rem;
  border: 1px solid var(--copper); border-radius: var(--radius);
  background: var(--panel); font-size: 0.92rem; line-height: 1.4;
}
.span-warn[hidden] { display: none; }
/* The fix is a real tap target inside the sentence. .link-button is excluded
   from the blanket 44px rule (it is usually inline in prose), so this one
   restores the height explicitly — it is the control Adam most needs to hit. */
.admin .span-warn .span-fix {
  display: inline-flex; align-items: center; min-height: 2.75rem;
  padding: 0 0.25rem; font-weight: 600;
}
.span-summary:empty { display: none; }
/* 0153 — the start time. One select (a phone's own wheel; 16px so iOS doesn't
   zoom) and a row of quick picks that set it. The pressed pick is the value. */
.span-time { display: grid; gap: 0.45rem; margin-top: 0.2rem; }
.admin .span-time select { width: 100%; min-height: 2.75rem; font-size: 1rem; }
@media (min-width: 26rem) { .admin .span-time select { max-width: calc(50% - 0.3rem); } }
.span-time-quick { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.admin .span-time-chip {
  min-height: 2.75rem; padding: 0 0.85rem; border-radius: 999px;
  border: 1px solid var(--border-strong); background: var(--panel);
  color: var(--muted); font: inherit; font-size: 0.92rem; cursor: pointer;
}
.admin .span-time-chip:hover:not(:disabled) { color: var(--text); background: var(--panel-2); }
.admin .span-time-chip[aria-pressed="true"] {
  color: var(--text); border-color: var(--copper); background: var(--panel-2); font-weight: 600;
}
.admin .span-time-chip:focus-visible { outline: 2px solid var(--copper); outline-offset: 1px; }
.admin .span-time-chip:disabled, .admin .span-time select:disabled { opacity: 0.5; cursor: not-allowed; }

/* NO WEEKENDS on the Schedule board. Bold and uppercase because it is an
   instruction a crew acts on, not a status — and it must not read as one of
   the status badges beside it. */
.no-weekends-tag {
  display: inline-block; font-weight: 800; letter-spacing: 0.04em;
  text-transform: uppercase; font-size: 0.78rem; white-space: nowrap;
  color: var(--copper-bright);
  border: 2px solid var(--copper); border-radius: 4px; padding: 0.05rem 0.4rem;
}

/* --- The Schedule board (6c) ----------------------------------------------
   An agenda, not a month grid: Adam reads this on a phone, and a GC job is a
   multi-day SPAN that a grid renders as a bar you can see but can't read. Cards
   stack on a narrow screen and gain a date rail once there's width for one. */
.sch-summary { margin: 0 0 0.8rem; color: var(--muted); }
.sch-summary strong { color: var(--text); font-family: var(--font-display); font-size: 1.1rem; }
.sch-controls { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 1.1rem; }
.admin .sch-search { flex: 1 1 14rem; min-height: 2.75rem; max-width: 26rem; }

.sch-group { margin-bottom: 1.6rem; }
.sch-group-h {
  display: flex; align-items: baseline; gap: 0.6rem;
  margin: 0 0 0.15rem; font-size: 1.05rem;
}
.sch-count {
  font-family: var(--font-body); font-size: 0.78rem; font-weight: 400;
  color: var(--muted); border: 1px solid var(--border-strong);
  border-radius: 999px; padding: 0.05rem 0.5rem;
}
.sch-group-note { margin: 0 0 0.5rem; font-size: 0.86rem; }
.sch-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.6rem; }

.sch-job {
  display: grid; gap: 0.5rem;
  padding: 0.8rem 0.9rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--radius);
}
/* Dates first and in the price colour: on this page the WHEN is the subject. */
.sch-dates { display: block; font-family: var(--font-display); font-weight: 700; color: var(--price); }
.sch-days { display: block; font-size: 0.82rem; }
/* 0153 — the crew's start, under the dates; shown only when one is set. */
.sch-time { display: block; font-size: 0.9rem; font-weight: 600; }
.sch-title { margin: 0; font-weight: 600; }
.sch-meta { margin: 0.1rem 0 0; font-size: 0.9rem; }
/* 0091 — the line a work unit fulfils. QUIET: it sits under the title in the
   muted voice, because the title is what a person reads first and this answers
   the question after it. Not a badge — badges on this row mean status, and a
   line name is not a state. */
.sch-line {
  margin: 0.1rem 0 0;
  font-size: 0.85rem;
  color: var(--muted);
}
.sch-line::before { content: "↳ "; opacity: 0.7; }
.sch-badges { margin: 0.35rem 0 0; display: flex; gap: 0.4rem; flex-wrap: wrap; }
.sch-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
/* 0137 — a row's comments fold out across the whole row, under the date rail,
   the detail and the actions, rather than squeezing into one grid column. */
.sch-job > .act-section { grid-column: 1 / -1; margin: 0.2rem 0 0; }
/* 0138 — Pay & progress on a board row: the same fold shape as the comments
   under it, so the two read as a pair. */
.sch-fold {
  grid-column: 1 / -1; margin: 0.2rem 0 0;
  background: var(--panel); border: 1px solid var(--border);
  border-left: 3px solid var(--copper); border-radius: var(--radius);
}
.sch-fold-h {
  padding: 0.7rem 0.9rem; min-height: 2.75rem; box-sizing: border-box;
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 0.95rem; color: var(--steel); cursor: pointer;
}
.sch-fold[open] > .sch-fold-h { border-bottom: 1px solid var(--border); color: var(--text); }
.sch-work-body { padding: 0.7rem 0.9rem; }

/* THE WORK PANEL (0138). Agreed · Paid · Still owed as three labelled figures,
   the buttons directly under them, the form under those. The owed figure is
   the one in the accent colour: it is the number a payment is measured by. */
.wp { display: grid; gap: 0.7rem; min-width: 0; }
.wp-money {
  margin: 0; display: grid; gap: 0.5rem;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
.wp-fig { min-width: 0; padding: 0.45rem 0.6rem; border: 1px solid var(--border); border-radius: var(--radius); }
.wp-fig dt { font-size: 0.75rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.wp-fig dd { margin: 0.1rem 0 0; font-weight: 700; overflow-wrap: anywhere; }
.wp-fig--owed { border-color: var(--copper); }
.wp-fig--owed dd { color: var(--copper-bright); }
.wp-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.admin .wp-actions .btn, .admin .wp-form-foot .btn { min-height: 2.75rem; }
.wp-actions:empty { display: none; }
.wp-note, .wp-msg, .wp-none { margin: 0; font-size: 0.9rem; }
.wp-form {
  display: grid; gap: 0.55rem; padding: 0.7rem; min-width: 0;
  border: 1px solid var(--border-strong); border-radius: var(--radius); background: var(--bg);
}
.wp-form[hidden] { display: none; }
.wp-form-h { margin: 0; font-weight: 700; }
.wp-form .inv-label { min-width: 0; }
.wp-form input, .wp-form select { font-size: 1rem; min-height: 2.75rem; max-width: 100%; box-sizing: border-box; }
.wp-form-row { display: grid; gap: 0.55rem; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); }
/* The live balance line, under the amount: says what this payment leaves. */
.wp-after { margin: -0.2rem 0 0; font-size: 0.9rem; font-weight: 600; color: var(--copper-bright); }
/* WO-0080 — "Apply credit?" (both payment forms), the job dialog's reduce
   question and the cancel dialog's keep-as-credit line: one boxed line and a
   tick box, the same in every place a sub's credit is asked about. */
.pay-credit {
  display: grid; gap: 0.15rem; padding: 0.45rem 0.7rem; min-width: 0;
  border: 1px solid var(--border-strong); border-left: 3px solid var(--copper); border-radius: var(--radius);
}
.pay-credit[hidden] { display: none; }
.pay-credit-has { margin: 0; font-size: 0.9rem; font-weight: 600; }
.pay-credit-has:empty { display: none; }
.pay-credit-opt { min-width: 0; }
.pay-credit-opt span { overflow-wrap: anywhere; }
.pay-after { margin: -0.2rem 0 0; font-size: 0.9rem; font-weight: 600; }
.pay-after:empty { display: none; }
.wp-credit-note { margin: 0; font-size: 0.85rem; }
.wp-credit-note[hidden] { display: none; }
.sc-crew-credit-field[hidden] { display: none; }
.cost-credit { margin-top: 0.25rem; }
.cost-credit-row .cost-meta { font-style: italic; }
.cost-raise { display: grid; gap: 0.5rem; margin-top: 0.5rem; padding: 0.6rem 0.7rem; border: 1px solid var(--border-strong); border-radius: var(--radius); }
.cost-raise input { font-size: 1rem; min-height: 2.75rem; width: 100%; box-sizing: border-box; }
.cost-raise-foot { display: flex; flex-wrap: wrap; gap: 0.5rem; }
/* WO-0080 ruling p — the same form in Pay & progress. */
.wp-raise { display: grid; gap: 0.5rem; margin-top: 0.5rem; padding: 0.6rem 0.7rem; border: 1px solid var(--border-strong); border-radius: var(--radius); }
.wp-raise[hidden] { display: none; }
.wp-raise .inv-label { min-width: 0; }
.wp-raise input { font-size: 1rem; min-height: 2.75rem; width: 100%; box-sizing: border-box; }
/* WO-0080 rulings q and r — "Take the credit back first" (credit-take-back.js). */
.credit-takeback { display: grid; gap: 0.5rem; margin-top: 0.5rem; padding: 0.6rem 0.7rem; border: 1px solid var(--border-strong); border-radius: var(--radius); }
.credit-takeback .ctb-said { margin: 0; }
.credit-takeback input { font-size: 1rem; min-height: 2.75rem; width: 100%; box-sizing: border-box; }
.credit-takeback .btn { min-height: 2.75rem; justify-self: start; }
.cost-takeback[hidden] { display: none; }
.admin .cost-raise-foot .btn { min-height: 2.75rem; }
.wp-form-foot { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.wp-pays { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.3rem; font-size: 0.9rem; }
.wp-pay-row { display: grid; grid-template-columns: auto 1fr auto; gap: 0.6rem; align-items: baseline; min-width: 0; }
.wp-pay-what { min-width: 0; overflow-wrap: anywhere; }
.admin .sch-actions .btn { min-height: 2.75rem; font-size: 0.85rem; padding: 0.45rem 0.9rem; }
/* A job with dates against an unsigned contract gets the copper edge — the one
   thing on this board that wants a second look before a crew is committed. */
.sch-job--pending { border-left-color: var(--copper); }
/* The badge carries an emoji, and the display face (Barlow Condensed) has no
   glyph for it — without an emoji font in the stack it renders as a tofu box. */
.badge-warn {
  color: var(--copper-bright); border-color: var(--copper);
  font-family: var(--font-body), "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
}

/* --- Unpaid planned work (0063) ---------------------------------------------
   Scheduled Work earmarked from an estimate, before the client bought anything.
   It sits on the same board as real work, so the ONE job of these rules is to
   make it unmistakable at a glance which is which.

   DASHED, not just a different colour. The board already spends its left edge
   on meaning (copper = pending signature), and a second solid colour would be
   one more thing to learn; a dashed edge reads as provisional before you have
   read a single word, and it survives being printed, dimmed, or looked at by
   someone who doesn't know the colour code. The .badge-unpaid pill carries the
   word itself, in the muted grey the vocabulary already uses for "not money
   yet" — deliberately NOT the --price green, which on this admin means real
   money, and not a warning red, because planned work is not a problem. */
.sch-job--placeholder { border-left-style: dashed; border-left-color: var(--steel); }
.sch-job--placeholder .sch-dates { color: var(--muted); }
/* The estimate behind it died. Warm edge, matching the badge it gains. */
.sch-job--stale { border-left-color: var(--copper); }
.badge-unpaid { color: var(--steel); border-color: var(--border-strong); }

/* The strip on the estimate. Deliberately quiet — it is a list of intentions
   under a quote, not a section of the document. */
.pw-h-note { font-family: var(--font-body); font-size: 0.85rem; text-transform: none; letter-spacing: 0; }
.pw-list { list-style: none; padding: 0; margin: 0 0 0.75rem; display: grid; gap: 0.5rem; }
.pw-item {
  padding: 0.6rem 0.75rem; background: var(--panel);
  border: 1px dashed var(--border-strong); border-radius: var(--radius);
}
.pw-item--stale { border-color: var(--copper); }
.pw-what { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.pw-title { color: var(--text); font-weight: 600; }
.pw-meta { margin: 0.2rem 0 0.4rem; font-size: 0.9rem; }
.pw-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.admin .pw-actions .btn-quiet { min-height: 2.5rem; font-size: 0.85rem; }
.pw-warn { margin: 0 0 0.6rem; color: var(--copper-bright); font-size: 0.9rem; }
/* 0066 — an orphaned unit in the reduced strip: planning that points at a line
   the estimate no longer has. Dashed copper rather than a new colour; it is the
   same "needs a look" the stale treatment already means. */
.pw-item--orphan { border-color: var(--copper); }
.pw-orphan-note { margin: 0 0 0.5rem; color: var(--copper-bright); font-size: 0.85rem; }
.pw-repoint { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; }
.admin .pw-repoint select { min-height: 2.5rem; font-size: 0.85rem; max-width: 20rem; }
.pw-repoint-none { font-size: 0.85rem; }
.pw-note { margin: 0 0 0.6rem; font-size: 0.9rem; }
.pw-total { margin: 0; font-size: 0.9rem; }
/* Planned crew cost in the profit panel. Muted on BOTH halves, so it reads as
   an aside beside the projected-profit row rather than a second bottom line. */
.est-profit-grid .ep-planned { color: var(--muted); }
/* The dialog's "planned only" note, under the pay field. */
.sw-hint { margin: -0.35rem 0 0; font-size: 0.85rem; }

@media (min-width: 44rem) {
  /* Date rail | detail | actions. The rail is fixed-width so every row's dates
     line up vertically and the column can be scanned on its own. */
  .sch-job { grid-template-columns: 11rem 1fr auto; align-items: start; gap: 0.9rem; }
  .sch-actions { justify-content: flex-end; }
}
/* The form is already styled by .client-form; inside the modal it just fills. */
.modal-panel .client-form { max-width: none; margin: 0; padding: 0; background: none; border: 0; }

/* --- Client row quick actions ---
   The row's open-detail button and the action rail are SIBLINGS: a button can't
   legally nest inside a button, and keeping them apart means a tap on a quick
   action can't reach the row's handler. Built as a rail so contract/invoice
   actions can be added beside this one without reflowing the row. */
/* min-width:0 is the row's half of the long-string fix. The button below
   already carries it; the <li> did not, and as a grid item it defaulted to
   min-width:auto — so the row itself held the track open at 404px inside a
   350px container no matter how the button behaved. */
.client-item { display: flex; align-items: stretch; gap: 0.5rem; min-width: 0; }
.client-item .client-open { flex: 1 1 auto; min-width: 0; }
.client-row-actions { display: flex; align-items: center; gap: 0.35rem; flex: none; }
.admin .client-quick {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.75rem; height: 2.75rem; min-height: 2.75rem; padding: 0;
  background: transparent; color: var(--steel);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
}
.admin .client-quick:hover { color: var(--copper-bright); border-color: var(--copper); }
.clients-count strong { color: var(--text); }
/* minmax(0, 1fr), not 1fr: a bare 1fr track is floored at the min-content of
   its widest item, which is what let one long email widen the whole page. */
.client-list { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.6rem; }
.client-list li.muted { padding: 0.25rem 0; }
.admin .client-open {
  display: flex; flex-direction: column; gap: 0.2rem; width: 100%; text-align: left;
  padding: 0.8rem 0.9rem; min-height: 2.75rem;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer;
  font-family: var(--font-body); font-weight: 400; text-transform: none; letter-spacing: 0;
}
.admin .client-open:hover { border-color: var(--steel); }
.client-row-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.client-row-head strong { color: var(--text); }
.client-row-contact { color: var(--steel); font-size: 0.9rem; }
.client-row-meta { color: var(--muted); font-size: 0.85rem; }

.client-detail { max-width: 40rem; }
.admin .client-back { min-height: 2.75rem; font-size: 0.85rem; padding: 0.45rem 0.9rem; margin-bottom: 1rem; }
.client-detail-head { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 0.9rem; }
.client-detail-head h2 { margin: 0; font-size: 1.3rem; }
.client-fields { display: grid; grid-template-columns: 8rem 1fr; gap: 0.5rem 1rem; margin: 0; }
.client-fields dt { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.75rem; color: var(--muted); align-self: center; }
.client-fields dd { margin: 0; color: var(--text); }
.client-fields a { color: var(--copper-bright); }
.client-notes { white-space: pre-wrap; color: var(--muted); }
.client-detail-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; margin-top: 1rem; }
.admin .client-detail-actions .btn { min-height: 2.75rem; font-size: 0.9rem; padding: 0.5rem 1rem; }
.client-docs { padding: 0.9rem 1rem; background: var(--panel); border: 1px dashed var(--border-strong); border-radius: var(--radius); }
.client-docs p { margin: 0; }
.client-est-list { list-style: none; padding: 0; margin: 0 0 0.9rem; display: grid; gap: 0.4rem; }
.client-est-item { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.client-est-total { color: var(--price); font-family: var(--font-display); font-weight: 700; }
.client-est-date { font-size: 0.85rem; }
.admin a.client-est-new { min-height: 2.75rem; font-size: 0.9rem; padding: 0.5rem 1rem; text-decoration: none; }

/* --- The client record's sections (0134b) ---
   Ten sections, the same ten on every client, so the page has one shape a
   reader can learn. An empty one still draws its heading and says "None" —
   which is why .cs-none is styled as an answer rather than as an absence.

   ⚠️ EVERY ROW IS A TAP TARGET AND CLEARS 44px. .cs-row is used by an <a> (a
   record that has a page), a <div> (one that does not) and a <button> (a
   document, whose URL is minted on the click). One class, three elements, so
   the three cannot drift apart visually — a row that looks identical and
   behaves differently is the confusion this page exists to remove. */
.cs-h { scroll-margin-top: 1rem; }
.cs-body { margin-bottom: 0.5rem; }
.cs-none { margin: 0; }
.cs-sub {
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 0.78rem; color: var(--muted);
  margin: 1.1rem 0 0.5rem;
}
.cs-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.4rem; }
.cs-item { margin: 0; }

.admin .cs-row {
  display: flex; flex-direction: column; gap: 0.25rem; width: 100%; text-align: left;
  padding: 0.6rem 0.8rem; min-height: 2.75rem; justify-content: center;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius);
  font-family: var(--font-body); font-weight: 400; text-transform: none; letter-spacing: 0;
  font-size: 0.95rem; text-decoration: none; cursor: pointer;
}
.admin a.cs-row:hover, .admin button.cs-row:hover { border-color: var(--steel); }
/* A row with nowhere to go does not pretend otherwise. */
.admin .cs-row.is-flat { cursor: default; }
.admin .cs-row.is-flat:hover { border-color: var(--border); }
.admin .cs-row[disabled] { opacity: 0.6; cursor: progress; }
.cs-row-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.cs-row-total { font-family: var(--font-display); font-weight: 700; color: var(--price); }
.cs-row-meta { font-size: 0.85rem; }

/* The money basis, said out loud. Three bases exist for "what is this client
   worth" and they are not the same number, so the label is not decoration —
   it is the difference between this page and Financials agreeing or not. */
.cs-basis { margin: 0 0 0.6rem; color: var(--muted); font-size: 0.9rem; }
.cs-basis-name { color: var(--text); text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.8rem; }
.cs-definition { margin: 0.6rem 0 0; font-size: 0.85rem; line-height: 1.5; }
.cs-figures { margin-bottom: 0.4rem; }

.cs-contact { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin: 0 0 0.3rem; }
.admin .cs-contact-name { min-height: 2.75rem; font-size: 0.95rem; }
.cs-contact-reach { margin: 0; color: var(--steel); font-size: 0.9rem; }
.cs-contact-reach a { color: var(--copper-bright); }
.cs-lead-link { color: var(--copper-bright); }
.cs-doc-msg { margin: 0.6rem 0 0; }

/* On a phone the row's two lines already stack; the head is what needs room,
   so the total drops under its badge rather than squeezing beside it. */
@media (max-width: 30rem) {
  .cs-row-head { gap: 0.35rem; }
  .cs-sub { margin-top: 0.9rem; }
}

/* --- Admin estimates (Operations) ---
   Same tap-and-go rules as Clients: 16px inputs, 44px+ targets. The line editor
   collapses from a 5-column grid to stacked, labelled fields on a phone. */
.est-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem 1rem; flex-wrap: wrap; margin-bottom: 0.85rem; }
.est-count { color: var(--muted); }
.est-count strong { color: var(--text); }
.est-head-actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.admin .est-head-actions .btn-quiet { min-height: 2.75rem; }
.admin .est-head-actions .btn { min-height: 2.75rem; font-size: 0.9rem; padding: 0.5rem 1rem; }

.est-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.6rem; }
.admin .est-open {
  display: flex; flex-direction: column; gap: 0.3rem; width: 100%; text-align: left;
  padding: 0.8rem 0.9rem; min-height: 2.75rem;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer;
  font-family: var(--font-body); font-weight: 400; text-transform: none; letter-spacing: 0;
}
.admin .est-open:hover { border-color: var(--steel); }
.est-row-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.est-row-foot { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; }
.est-row-total { font-family: var(--font-display); font-weight: 700; color: var(--price); }
.est-row-date { color: var(--muted); font-size: 0.85rem; }
/* 0097 — has the client opened it? One glyph, in the row's date lane.
   HOLLOW = not opened, FILLED = opened. Both states are drawn, because an
   absent dot would be indistinguishable from a row that was never sent, and
   "we don't know" and "nobody has looked" are different answers. Colour is not
   the signal — the fill is — and the sentence rides in the title and an
   .sr-only span beside it, so nothing here is conveyed by shape alone. */
.est-view-dot {
  display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 50%;
  border: 1px solid var(--muted); background: transparent;
  flex: 0 0 auto; align-self: center;
}
.est-view-dot.is-open { background: var(--copper); border-color: var(--copper); }

.admin .est-back { min-height: 2.75rem; font-size: 0.85rem; padding: 0.45rem 0.9rem; margin-bottom: 1rem; }
.est-builder-head { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 0.35rem; }
.est-builder-head h2 { margin: 0; font-size: 1.3rem; }
.est-meta { color: var(--muted); font-size: 0.9rem; margin: 0 0 1rem; }
.est-client-label { display: flex; flex-direction: column; gap: 0.25rem; max-width: 24rem; margin-bottom: 1.25rem; }
.admin .est-client-label select { min-height: 2.75rem; }

.est-lines { display: grid; gap: 0.6rem; margin-bottom: 0.75rem; }
/* Each line item reads as its OWN block: air between neighbours and a copper
   left edge — the same accent the profit box, the client note and the library's
   add-form already use, so this is the house marker rather than a new one. The
   copper is an edge, not a fill: it separates without shouting.
   Both line editors get it (estimate builder and contract editor share this
   component); the rows touched edge-to-edge in both before. */
.est-line {
  display: grid; gap: 0.5rem;
  padding: 0.8rem 0.9rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: 3px solid var(--copper);
  border-radius: var(--radius);
}
.est-line + .est-line { margin-top: 0.75rem; }

/* --- .ws-line-bar — THE ROW-SCALE BAR (0076) ---------------------------------
   The theme's third consumer and its first at row scale. A saved estimate line
   at rest: the builder opens with every line like this and Adam opens the one he
   came to change.

   ⚠️ display:block IS THE FIX, AND IT IS LOAD-BEARING. This row lives inside
   `.est-line`, which it must keep — bindLineEditor and every delegated handler
   key off that class and off data-i (the 0069 rail). But .est-line is a GRID,
   and three media queries give it explicit columns: five equal fractions at
   ≥46rem, two at 34–46rem, one below. The collapsed row has a single child, so
   at desktop that child landed in COLUMN 1 OF 5 — about 220px — and its own
   inner grid needs 237px of fixed columns and padding before the name gets
   anything. minmax(0, 1fr) floored the name at zero width and the text wrapped
   one character per line: "MainPad" as a vertical letter-stack, the row four
   fifths empty.

   Resetting grid-template-columns would mean repeating the reset in all three
   media queries and hoping nobody adds a fourth. Changing the formatting context
   ends it once: block, and the bar inside is width:100%. */
.est-line--collapsed { display: block; padding: 0; gap: 0; border-left-color: var(--border-strong); }
.est-line--collapsed:hover { border-left-color: var(--copper); }

.admin .ws-line-bar {
  display: grid; width: 100%; min-width: 0;
  grid-template-columns: 0.9rem 2rem minmax(6rem, 1fr) 4rem 7rem;
  gap: 0.6rem; align-items: center; text-align: left;
  min-height: 2.75rem; padding: 0.6rem 0.85rem;
  background: transparent; color: var(--text); border: 0; border-radius: 0;
  font-family: var(--font-body); font-weight: 400; text-transform: none; letter-spacing: 0;
  cursor: pointer;
}
.admin .ws-line-bar:hover { background: var(--panel-2); }
.admin .ws-line-bar:active { background: var(--panel); }
.admin .ws-line-bar:focus-visible { outline: 2px solid var(--copper); outline-offset: -2px; }

/* The chevron, at row weight — smaller than .ws-bar-mark, same idea and the
   same borders-not-a-glyph trick, so it inherits colour and needs no font. */
.ws-line-mark {
  justify-self: start; width: 0; height: 0;
  border-left: 6px solid var(--copper);
  border-top: 4px solid transparent; border-bottom: 4px solid transparent;
}
/* 0098 — THE SAME MARK, TURNED. The open row's header carries this variant so
   the control that closes a line is the control that opened it, rotated: same
   element, same grid column, same 6x8 border-triangle, pointing down instead of
   right. A separate glyph or a swapped element would be a second control in the
   same place, which is the impression this build exists to remove. It rotates
   about its own centre and the column is 0.9rem, so nothing shifts. */
.ws-line-mark--open { transform: rotate(90deg); }
.ws-line-pos { font-size: 0.9rem; color: var(--muted); }
/* FLEXIBLE, AND IT TRUNCATES. minmax(6rem, 1fr) guarantees a floor no fixed
   column can take away; the ellipsis handles a genuinely long name without
   letting it push the money out of the row. */
.ws-line-name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text);
}
.ws-line-qty { font-size: 0.9rem; color: var(--muted); text-align: right; }
.ws-line-total {
  font-family: var(--font-display); font-weight: 700; color: var(--price); text-align: right;
}
/* Chips get their own row under the name — a crew summary is a name, a date
   span and a figure, and it will not share a line with the total. Read-only
   here; one click opens the row to edit them, which is what keeps 0064's photos
   and 0066's crew reachable. */
.ws-line-chips {
  grid-column: 3 / -1; display: flex; flex-wrap: wrap; align-items: center;
  gap: 0.35rem; margin-top: 0.3rem; font-size: 0.85rem;
}
.el-chip { font-size: 0.85rem; }
/* "Done", "Remove" and "Save to library" are dressed together in THE LINE
   EDITOR'S LAYOUT block below, with the rest of the footer's action row. */

@media (max-width: 34rem) {
  /* Per the theme's stated phone behaviour: the name keeps the row with the
     chevron and the number, and the money folds underneath rather than
     competing for width. */
  .admin .ws-line-bar { grid-template-columns: 0.9rem 1.6rem minmax(0, 1fr) auto; }
  .ws-line-qty { grid-column: 3; text-align: left; font-size: 0.85rem; }
  .ws-line-total { grid-column: 4; }
  .ws-line-chips { grid-column: 1 / -1; }
}

/* 0066 — the crew strip: a band in the line editor's FOOTER saying who is doing
   this piece of work. Its placement lives with every other child's, in THE LINE
   EDITOR'S LAYOUT block further down; what's here is the strip's own dress.
   Estimate builder only — the contract editor passes no crewFor and renders
   nothing here.

   ⚠️ 0077 — A STRAY `}` ABOVE THIS RULE DELETED IT FOR A WHOLE BUILD. 0076 left
   the old media query's closing brace behind, and a `}` at the top level is not
   ignored: the parser reads it as the start of a qualified rule and swallows
   component values until the next {} block — which was this one. So .el-crew
   was dropped whole (no flex, no full-width span, no 1px dashed rule) while
   .el-crew--empty below it, a rule of its own, still applied. A lone
   `border-top-style: dotted` with no width beside it computes to the initial
   `medium` — and THAT is the stray dashed fragment in Adam's screenshot,
   floating over an "Assign Crew" wrapped to two lines because the unplaced,
   no-longer-flex strip had auto-placed into the 3.5rem "#" column.
   A brace check on this file is cheap; a build of invisible cascade is not. */
.el-crew {
  display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  margin-top: 0.15rem; padding-top: 0.5rem;
  border-top: 1px dashed var(--border-strong);
  font-size: 0.9rem;
}
.el-crew-who { color: var(--text); }
/* Same "needs a look" copper the planned-work strip uses for a dead estimate. */
.el-crew--stale .el-crew-who { color: var(--copper-bright); }
.admin .el-crew .btn-quiet { min-height: 2.5rem; font-size: 0.85rem; }
/* Unassigned: just the button, quiet enough not to compete with Remove. */
.el-crew--empty { border-top-style: dotted; }
.est-line .el-f { display: flex; flex-direction: column; gap: 0.25rem; }
.admin .est-line input { font-size: 1rem; min-height: 2.75rem; }
/* Description is a resizable textarea (0035 batch item 1) — comfortable room to
   write multi-line descriptions, overriding the tall global textarea min-height
   so it still fits the compact line row, and growing on drag. */
.admin .est-line textarea.el-desc {
  font-size: 1rem; min-height: 2.75rem; width: 100%;
  resize: vertical; line-height: 1.4; padding: 0.5rem 0.6rem;
}
.admin .est-add-line { min-height: 2.75rem; }
.est-total-row { margin: 0 0 1.25rem; font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); }
.est-total-row strong { color: var(--price); font-size: 1.25rem; margin-left: 0.4rem; }
.admin .est-actions { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-top: 0.5rem; }
.admin .est-actions .btn { min-height: 2.75rem; font-size: 0.9rem; padding: 0.5rem 1rem; }
.est-status-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.admin .est-status-actions .btn { min-height: 2.75rem; font-size: 0.9rem; padding: 0.5rem 1rem; }
.est-convert { margin-top: 0.9rem; }
.admin .est-convert .btn { min-height: 2.75rem; font-size: 0.9rem; padding: 0.5rem 1rem; }

.est-convert-note { margin: 0.5rem 0 0; font-size: 0.85rem; max-width: 34rem; }
/* 0150 — the job start: what the system did after the approval. A failed or
   quiet one is the one that needs Adam, so it wears the warning edge. */
.est-job-start { margin-top: 0.9rem; padding: 0.75rem 0.9rem; border: 1px solid var(--border); border-left: 3px solid var(--border-strong); border-radius: var(--radius); max-width: 40rem; }
.est-job-start--quiet, .est-job-start--stalled, .est-job-start--starting { border-left-color: var(--copper); }
.est-job-start--done { border-left-color: #9FD8B0; }
.est-job-start-line { margin: 0; }
.est-job-start-links { margin: 0.4rem 0 0; font-size: 0.9rem; }
.est-job-start-actions { margin-top: 0.6rem; }
.admin .est-job-start-actions .btn { min-height: 2.75rem; }
/* WO-0009 (0166) — Pause reminders until…: under the status line and inside
   the job-start panel. The warning note is the "this runs past the last day"
   sentence, shown before the pause is saved. */
.est-pause { margin-top: 0.75rem; max-width: 40rem; }
.est-pause-line { margin: 0 0 0.5rem; }
.est-pause-form { display: grid; gap: 0.4rem; margin-top: 0.25rem; }
.est-pause-label { margin: 0; font-weight: 600; }
.est-pause-date { max-width: 14rem; min-height: 2.75rem; font: inherit; }
.est-pause-note { margin: 0; font-size: 0.9rem; }
.est-pause-note.est-pause-warn { color: inherit; border-left: 3px solid var(--copper); padding-left: 0.6rem; }
.est-pause-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.admin .est-pause .btn { min-height: 2.75rem; }
/* [hidden] loses to .btn's and the form's own display; say it here. */
.est-pause [hidden] { display: none; }
.est-preview-note { margin: 0.4rem 0 0; font-size: 0.85rem; max-width: 34rem; }

/* Estimate lifecycle tabs reuse the .inv-filter pill component. */
.est-tabs { flex: 1 1 auto; }
.est-note-flag { color: var(--copper-bright); border-color: var(--copper); }
.est-status-line { color: var(--muted); margin: 0 0 0.6rem; }
.est-status-line strong { color: var(--text); }
.est-client-note {
  background: var(--panel); border: 1px solid var(--border); border-left: 3px solid var(--copper);
  border-radius: var(--radius); padding: 0.6rem 0.8rem; margin: 0 0 0.9rem; color: var(--text); max-width: 40rem;
}
.est-client-note-h { display: block; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); margin-bottom: 0.2rem; }
/* The requested-changes callout at the top of the builder (0050). Louder than
   .est-client-note on purpose — this is a brief, not a footnote, and it is the
   reason Adam opened the screen. Amber rather than red: the estimate isn't
   lost, it's waiting on him. */
.est-changes {
  background: var(--panel); border: 1px solid var(--copper-bright);
  border-left: 4px solid var(--copper-bright); border-radius: var(--radius);
  padding: 0.8rem 1rem; margin: 0 0 1.1rem; max-width: 44rem;
}
.est-changes-h {
  margin: 0 0 0.4rem; font-family: var(--font-display); font-weight: 700;
  color: var(--copper-bright); font-size: 0.95rem;
}
/* pre-wrap: the client typed this in a textarea, so their line breaks are
   theirs to keep. overflow-wrap for a pasted URL. */
.est-changes-text {
  margin: 0 0 0.5rem; color: var(--text); line-height: 1.5;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.est-changes-hint { margin: 0; font-size: 0.82rem; }

/* --- The client's two other answers on the approval page (0050) --- */
/* Below Approve and quieter than it: all three are one tap, but the page must
   not present declining as an equal-weight default. */
.ev-alt { margin-top: 1.6rem; padding-top: 1.2rem; border-top: 1px solid var(--border); }
.ev-alt-lead { margin: 0 0 0.6rem; font-size: 0.9rem; }
.ev-alt-actions { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.ev-alt-form { margin-top: 1rem; }
/* Stacks at 390px without a media query — wrap does the work, and the buttons
   keep a thumb-sized target. */
.ev-alt-row { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center; }
.ev-alt-row .btn { min-height: 2.75rem; }
.est-preview-caption { margin: 0 0 0.8rem; font-size: 0.85rem; }
.modal-panel-wide { max-width: 42rem; }

/* Client-facing estimate view (0035) — the ONE renderer shared by the public
   /estimate/ page and the admin "Preview as client" modal. Themed via CSS
   variables so it reads correctly on the marketing page and in the dark admin
   modal alike. Mirrors the sign-page line layout on purpose. */
.ev-head { display: flex; flex-wrap: wrap; gap: 0.3rem 1rem; align-items: baseline; margin: 0 0 0.9rem; }
.ev-ref { font-family: var(--font-display); font-weight: 700; color: var(--text); }
.ev-client { color: var(--muted); font-size: 0.9rem; }
.ev-lines { list-style: none; padding: 0; margin: 0 0 1rem; display: grid; gap: 0.5rem; }
.ev-line {
  display: grid; grid-template-columns: 3.5rem 1fr 6rem 6rem; gap: 0.5rem; align-items: baseline;
  padding: 0.7rem 0.8rem; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
}
.ev-line-qty { color: var(--muted); }
.ev-line-desc { color: var(--text); }
/* ---- FIX B: the client-facing line name reads as a heading ------------------
   It was already on its own line (lineLabelHtml emits a conditional <br /> after
   it) and already <strong>, but carried no size or family of its own — so it
   rendered as body-size bold, indistinguishable from the description beneath
   it, while .ev-line-total right next to it is display-font, 700, in the price
   colour. The eye went to the money and the name read as body copy.

   display:block coexists with the existing <br /> rather than replacing it: the
   <br /> is emitted only when a description follows, so a name-only line still
   must not print an empty row under itself. That conditional stays in the JS,
   untouched. */
.ev-line-name {
  color: var(--text);
  font-size: 1.05rem;
  font-family: var(--font-display);
  letter-spacing: 0.01em;
  display: block;
  margin-bottom: 0.15rem;
}
.ev-line-disc { display: block; color: var(--muted); font-size: 0.82rem; margin-top: 0.2rem; }
.ev-line-unit { color: var(--muted); font-size: 0.9rem; text-align: right; }
.ev-line-total { font-family: var(--font-display); font-weight: 700; color: var(--price); text-align: right; }
.ev-subline { display: flex; justify-content: space-between; gap: 1rem; color: var(--muted); margin: 0.15rem 0 0.15rem auto; max-width: 22rem; }
.ev-total { font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); text-align: right; margin: 0.4rem 0 1rem; }
.ev-total strong { color: var(--price); font-size: 1.3rem; margin-left: 0.5rem; }
.ev-notes { color: var(--muted); font-size: 0.85rem; max-width: 40rem; margin: 0.5rem 0 0; }
.ev-notes-h { font-size: 0.9rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em; margin: 0 0 0.4rem; }
.ev-notes p { margin: 0 0 0.4rem; }
@media (max-width: 34rem) {
  .ev-line { grid-template-columns: 3rem 1fr; }
  .ev-line-unit, .ev-line-total { text-align: left; grid-column: 2; }
}

/* Line photos (0040; a set of up to four since 0048) — the admin control, the
   client-facing thumbnail row, and the shared lightbox. */
/* Unscoped from .el-f-img on purpose: the same gallery is rendered inside the
   editors' <label> AND in the read-only line list a sent/signed contract shows,
   and one rule keeps the two looking identical. */
.el-img-set { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.el-img-thumb { display: block; width: 3.25rem; height: 3.25rem; object-fit: cover; border-radius: var(--radius); border: 1px solid var(--border); cursor: zoom-in; }
/* A photo whose signed URL wouldn't mint: the slot stays (so its × can remove
   it) with nothing in it. */
.el-img-thumb--blank { background: var(--border); cursor: default; }
/* Each photo carries its own remove control, tucked onto the thumbnail's
   corner. 1.5rem square is small on a desktop but still a comfortable phone
   target, which is where these photos get taken. */
.el-img-item { position: relative; display: inline-flex; }
.el-img-remove {
  position: absolute; top: -0.35rem; right: -0.35rem;
  width: 1.5rem; height: 1.5rem; padding: 0; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1rem; color: var(--text); background: var(--surface);
  border: 1px solid var(--border-strong); border-radius: 50%; cursor: pointer;
}
.el-img-remove:hover { background: var(--danger, #a33); color: #fff; }
.el-img-full { font-size: 0.8rem; }
/* Photos shown WITHOUT the upload control (the contract editor): a plain
   <span>, not a <label>, so it doesn't read as a form field for something that
   can't be typed into. */
.el-f-img--ro { cursor: default; }
/* The read-only line list. .ro-line is a 4-column grid with 5 children now, so
   the photo cell already falls to a second row; placing it from column 2 to the
   end lines it up under the description instead of squeezing it into the 3.5rem
   qty column, and holds on the 2-column mobile grid. It renders nothing at all
   for a line with no photos, so those lines are unchanged. */
.ro-imgs { grid-column: 2 / -1; display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.35rem; }

/* The client's view: a row of thumbnails under the line's description, wrapping
   on a narrow screen. The line is a 4-column grid with 5 children, so the photo
   cell already falls to a second row; placing it from column 2 to the end lines
   it up beneath the description instead of squeezing it into the 3.5rem qty
   column, and holds on the 2-column mobile grid too. It collapses to nothing
   when the line has no photos, so those lines are unchanged. */
.ev-line-img, .sign-line-img {
  display: inline-flex; align-items: center;
  grid-column: 2 / -1;
}
.ev-line-img:empty, .sign-line-img:empty { display: none; }
.ev-thumbs { display: inline-flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.35rem; }
.ev-thumb {
  display: block; padding: 0; line-height: 0; background: none;
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  cursor: zoom-in; overflow: hidden;
}
.ev-thumb img { display: block; width: 2.75rem; height: 2.75rem; object-fit: cover; }
.ev-thumb:hover { border-color: var(--steel); }
.ev-thumb:focus-visible { outline: 2px solid var(--copper); outline-offset: 2px; }

.lightbox {
  position: fixed; inset: 0; z-index: 200; display: flex; align-items: center; justify-content: center;
  padding: 1.5rem; background: rgba(10, 8, 6, 0.85); cursor: zoom-out;
}
.lightbox-img { max-width: 96vw; max-height: 92vh; border-radius: 6px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); }
.lightbox-close {
  position: absolute; top: 0.75rem; right: 1rem; font-size: 2rem; line-height: 1;
  background: transparent; border: 0; color: #fff; cursor: pointer; padding: 0.25rem 0.5rem;
}
body.lightbox-open { overflow: hidden; }

/* Dashboard — approved estimates ready to convert (0035). */
.appr-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.7rem; }
.appr-card { display: flex; flex-wrap: wrap; gap: 0.75rem 1rem; align-items: center; justify-content: space-between; }
.appr-main { flex: 1 1 16rem; min-width: 0; }
.appr-client { font-weight: 600; color: var(--text); margin: 0 0 0.15rem; }
.appr-meta { display: flex; gap: 0.6rem; flex-wrap: wrap; align-items: baseline; margin: 0; color: var(--muted); font-size: 0.9rem; }
.appr-total { font-family: var(--font-display); font-weight: 700; color: var(--price); }
.appr-note { margin: 0.5rem 0 0; padding: 0.5rem 0.7rem; background: var(--panel); border-left: 3px solid var(--copper); border-radius: var(--radius); color: var(--text); font-size: 0.9rem; }
.appr-note-h { display: block; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); margin-bottom: 0.15rem; }
.appr-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.admin .appr-actions .btn { min-height: 2.75rem; font-size: 0.9rem; padding: 0.5rem 1rem; }

/* Contractors — specialty filter (batch item 4) + company subtitle (item 3). */
.contractor-filters { margin: 0 0 0.75rem; }
.contractor-filter-label { display: flex; flex-direction: column; gap: 0.25rem; max-width: 18rem; }
.admin .contractor-filter-label select { min-height: 2.75rem; }
.contractor-company { margin: -0.3rem 0 0.6rem; }

/* Leads — Open/Closed tabs + inline notes log (0042). */
.lead-tabs { margin-bottom: 0.85rem; }
.lead-item.is-closed { opacity: 0.82; }
/* Arrived here from the Estimates / Leads tab's leads strip (?id=…). The scroll
   alone leaves you looking at a list and guessing which row you were sent to, so
   the target holds a copper edge for a moment and then lets go. Colour only — no
   layout property, so nothing below it moves while the page settles. */
.lead-item.is-linked {
  border-color: var(--copper);
  box-shadow: 0 0 0 1px var(--copper);
}
@media (prefers-reduced-motion: no-preference) {
  .lead-item { transition: border-color 0.4s ease, box-shadow 0.4s ease; }
}
.lead-log-wrap { margin-top: 0.6rem; }
.lead-log-add { display: flex; gap: 0.4rem; margin: 0.25rem 0 0.45rem; }
.lead-log-add .lead-note-input { flex: 1 1 auto; min-height: 2.5rem; }
.admin .lead-log-add .lead-note-save { min-height: 2.5rem; white-space: nowrap; }
.lead-log {
  list-style: none; margin: 0; padding: 0.4rem 0.55rem; max-height: 9rem; overflow-y: auto;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
  display: grid; gap: 0.4rem;
}
.lead-log-item { display: grid; gap: 0.1rem; padding-bottom: 0.35rem; border-bottom: 1px solid var(--border); }
.lead-log-item:last-child { border-bottom: 0; padding-bottom: 0; }
.lead-log-time { font-size: 0.72rem; color: var(--muted); }
.lead-log-body { color: var(--text); font-size: 0.9rem; white-space: pre-wrap; }
.lead-log-empty { border-bottom: 0; padding: 0; }

/* Contract completion + loss (0043). filed_as_loss reads distinct from void. */
.badge-loss { color: #E89A8A; border-color: #7A3B32; }
.ct-completion { margin: 0 0 0.6rem; }
.ct-completion-note { color: var(--muted); margin: 0 0 0.5rem; }
.ct-loss { background: var(--panel); border: 1px solid var(--border); border-left: 3px solid #7A3B32; border-radius: var(--radius); padding: 0.8rem 0.9rem; max-width: 34rem; }
.ct-loss-head { margin: 0 0 0.5rem; display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.ct-loss-facts { margin: 0 0 0.4rem; }
/* WO-0004 — a cancelled job: the loss block's shape, the cancel badge's grey —
   closed, not completed, and not a loss. */
.ct-cancelled { border-left-color: var(--border-strong); }
.admin button.btn-ghost-danger { color: #E79A8A; }
/* The Cancel dialog (cancel-contract-modal.njk). */
.cc-lead { margin: 0 0 0.6rem; color: var(--steel); font-size: 0.9rem; }
.cc-refusal { margin: 0 0 0.6rem; padding: 0.55rem 0.7rem; border: 1px solid #7A3B32; border-radius: var(--radius); color: #E89A8A; }
.cc-group { border: 0; padding: 0; margin: 0 0 0.8rem; min-width: 0; }
.cc-group legend { padding: 0; }
.cc-collected { margin: 0.1rem 0 0.3rem; }
.cc-payments { margin: 0 0 0.5rem; padding-left: 1.1rem; font-size: 0.88rem; color: var(--muted); }
.cc-part { margin: 0 0 0.4rem 1.9rem; }
.cc-work { margin: 0.1rem 0 0; }

/* WO-0071 #46 — the archive dialog (client-archive-modal.njk), the archived
   client's banner, and the line that says what the dialog did. The refusals
   take the Cancel dialog's red edge; the banner is a state, so it is calm. */
.ca-blocked { margin: 0 0 0.8rem; }
.ca-refusals { list-style: none; margin: 0 0 0.6rem; padding: 0; display: grid; gap: 0.5rem; }
.ca-refusal { padding: 0.55rem 0.7rem; border: 1px solid #7A3B32; border-radius: var(--radius); min-width: 0; overflow-wrap: anywhere; }
.ca-refusal p { margin: 0; }
.ca-meta { display: flex; flex-wrap: wrap; gap: 0.2rem 0.8rem; margin: 0.25rem 0 !important; font-size: 0.88rem; }
.ca-cents { font-family: var(--font-display); font-weight: 700; color: var(--price); }
.ca-plate { color: var(--muted); }
.ca-next { font-size: 0.88rem; }
.ca-guided { margin: 0.2rem 0 0; }
.ca-warnings { margin: 0 0 0.6rem; padding-left: 1.1rem; font-size: 0.88rem; color: var(--copper-bright); }
.ca-figures { margin: 0 0 0.6rem; padding: 0.55rem 0.7rem; border: 1px solid #6E5B34; border-left: 3px solid #E7C98A; border-radius: var(--radius); }
.ca-hides { margin: 0 0 0.6rem; font-size: 0.88rem; }
.ca-banner { margin: 0 0 1rem; padding: 0.7rem 0.9rem; background: var(--panel); border: 1px solid var(--border); border-left: 3px solid var(--border-strong); border-radius: var(--radius); max-width: 40rem; overflow-wrap: anywhere; }
.ca-banner p { margin: 0 0 0.35rem; }
.ca-banner .sc-confirm { margin-top: 0.5rem; }
.ca-notice { margin: 0 0 0.8rem; padding: 0.6rem 0.8rem; border: 1px solid #7A3B32; border-radius: var(--radius); color: #E89A8A; overflow-wrap: anywhere; }
.ca-notice.is-ok { border-color: var(--border-strong); color: var(--text); }
.ct-loss-reason { margin: 0.3rem 0 0.6rem; }
/* 0082 — .ct-loss-panel and .ct-loss-warn are GONE with the inline write-off
   panel they styled. The write-off is a branch of the close dialog now, and
   dead rules for markup nothing emits are how a stylesheet stops being
   readable. The .ct-loss BLOCK above stays: it renders a job that HAS been
   written off, which is a state, not a control. */

/* --- Admin contracts (Operations 3a) ---
   Reuses the estimates list/builder classes; only the contract-specific bits
   live here. Read-only lines are what a sent or signed contract shows. */
.ct-facts { display: grid; grid-template-columns: 6rem 1fr; gap: 0.4rem 1rem; margin: 0 0 0.5rem; }
.ct-facts dt { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.75rem; color: var(--muted); align-self: center; }
.ct-facts dd { margin: 0; color: var(--text); }
.ct-total-fact { font-family: var(--font-display); font-weight: 700; color: var(--price); }
/* The project start date sits in the facts list but is an INPUT, not a value —
   it wraps to its own line on a narrow screen rather than squeezing the date
   picker and its save button into the 1fr column side by side. */
.ct-start { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.6rem; }
.ct-start-input {
  font: inherit; padding: 0.35rem 0.5rem; color: var(--text); background: var(--bg);
  border: 1px solid var(--border-strong); border-radius: var(--radius); min-height: 2.5rem;
}
.ct-start-input:focus-visible { outline: 2px solid var(--copper); outline-offset: 1px; }
.ct-start-hint { font-size: 0.8rem; margin: -0.2rem 0 0.8rem; max-width: 44rem; }
.ro-lines { list-style: none; padding: 0; margin: 0 0 0.75rem; display: grid; gap: 0.4rem; }
.ro-line { display: grid; grid-template-columns: 3.5rem 1fr 6rem 6rem; gap: 0.5rem; align-items: baseline; padding: 0.6rem 0.7rem; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); }
.ro-qty { color: var(--muted); }
.ro-desc { color: var(--text); }
/* Quiet marker on a tax-exempt line in the read-only view (0047) — a fact about
   the line, not a warning, so it sits in the muted register. */
.ro-notax {
  font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--muted); border: 1px solid var(--border);
  border-radius: 999px; padding: 0.05rem 0.4rem; white-space: nowrap;
}
.ro-unit { color: var(--muted); font-size: 0.9rem; text-align: right; }
.ro-total { font-family: var(--font-display); font-weight: 700; color: var(--price); text-align: right; }
.ct-sign { display: flex; flex-direction: column; align-items: flex-start; gap: 0.6rem; }
.ct-sign-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.admin .ct-sign .btn { min-height: 2.75rem; font-size: 0.9rem; padding: 0.5rem 1rem; }
.ct-sign p { margin: 0; }
.ct-signed { color: var(--text); }
.ct-awaiting { color: var(--copper-bright); }
/* The placeholder gate is a "not ready yet", not a failure — amber and calm,
   deliberately not the red .step-error treatment. */
.ct-not-ready {
  max-width: 40rem; padding: 0.9rem 1rem;
  background: var(--panel); border: 1px solid #6E5B34; border-left: 3px solid #E7C98A;
  border-radius: var(--radius);
}
.ct-not-ready p { margin: 0 0 0.5rem; color: var(--muted); }
.ct-not-ready p:last-child { margin-bottom: 0; }
.ct-not-ready-head { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.85rem; color: #E7C98A !important; }
.ct-not-ready code { font-size: 0.85rem; color: var(--text); }
.ct-signed-block { display: flex; flex-direction: column; align-items: flex-start; gap: 0.4rem; }
.ct-signed-block strong { color: var(--text); }
/* 0171 — "Client approved by phone…": the panel that records it, with its
   optional note. Same box language as the send confirm beside it. */
.ct-approve-panel {
  margin: 0.75rem 0 0;
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  display: grid;
  gap: 0.5rem;
}
.ct-approve-panel[hidden] { display: none; } /* display:grid above would override [hidden] */
.ct-approve-panel p { margin: 0; }
.ct-approve-panel textarea { width: 100%; }
.ct-approve-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.ct-approve-hint { font-size: 0.85rem; }

/* --- Change orders (Operations 8a) ---
   The list has one job beyond looking tidy: making it unmistakable which
   addenda are IN the contract total and which aren't. A signed one takes the
   price colour like every other real figure; an unsigned one stays muted and
   says so in words, because a number that looks like money but isn't yet is
   exactly what gets misread. */
/* --- The change order modal (0070) ------------------------------------------
   Authored on the Jobs page now, in place. Reuses the estimate builder's line
   rows wholesale (.est-line and friends), so this file only adds the parts that
   are new: the three chips, the pick-list, the per-line delta editors and the
   signed-sum summary. No new design language — badge, muted and panel
   vocabulary throughout. */
.modal-panel--wide { max-width: 46rem; }
.co-kind { display: flex; flex-wrap: wrap; gap: 0.5rem; border: 0; padding: 0; margin: 0 0 0.9rem; }
.co-kind > legend { padding: 0; margin-bottom: 0.4rem; font-size: 0.85rem; }
.co-kind-chip { display: inline-flex; }
.co-kind-chip input { position: absolute; opacity: 0; width: 0; height: 0; }
.co-kind-chip span {
  display: inline-block; min-height: 2.75rem; padding: 0.6rem 0.95rem;
  background: var(--panel); border: 1px solid var(--border-strong);
  border-radius: var(--radius); cursor: pointer; font-size: 0.9rem;
}
.co-kind-chip input:checked + span { background: var(--copper); border-color: var(--copper); color: #14110E; }
.co-kind-chip input:focus-visible + span { outline: 2px solid var(--copper-bright); outline-offset: 2px; }

.co-section { margin: 0 0 1rem; padding: 0.85rem 0.95rem; }
.co-section-h {
  margin: 0 0 0.4rem; font-family: var(--font-display); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.8rem; color: var(--steel);
}
.co-section-note { margin: 0 0 0.6rem; font-size: 0.85rem; }

/* --- WO-0018: three steps, a finder, and a preview ---------------------------
   Step 1 holds only what was picked; the finder opens, is picked from, and
   closes. Step 2 says where each change lands, before → after. Same badge,
   muted and panel vocabulary as the rest of the modal. */
.co-steps {
  list-style: none; counter-reset: co-step; display: flex; flex-wrap: wrap; gap: 0.35rem 1rem;
  margin: 0 0 0.9rem; padding: 0; font-size: 0.85rem; color: var(--muted);
}
.co-steps li { counter-increment: co-step; }
.co-steps li::before { content: counter(co-step) ". "; }
.co-steps li.is-current { color: var(--text); font-weight: 700; }
.co-steps li.is-done { color: var(--steel); }
.co-step-lead { margin: 0 0 0.75rem; font-size: 0.9rem; }
.co-empty { margin: 0 0 0.75rem; }
.co-step-tools { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0 0 0.75rem; }
.co-net-line { margin: 0.25rem 0 0.75rem; font-weight: 700; }
.co-list-staged { display: grid; gap: 0.6rem; margin: 0 0 0.75rem; }
.co-card { padding: 0.75rem 0.85rem; min-width: 0; }
.co-card-head { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.co-card-what { overflow-wrap: anywhere; }
.co-card-now { font-size: 0.85rem; }
.co-card-x, .admin button.co-card-x { margin-left: auto; min-width: 2.75rem; min-height: 2.75rem; font-size: 1.25rem; }
.co-card-desc { margin: 0.2rem 0 0; font-size: 0.85rem; overflow-wrap: anywhere; }
.co-card-mode { display: flex; flex-wrap: wrap; gap: 0.5rem; border: 0; padding: 0; margin: 0.5rem 0; }
.co-finder { padding: 0.75rem 0.85rem; margin: 0 0 0.75rem; }
.co-finder-head { display: flex; align-items: flex-end; gap: 0.5rem; }
.co-finder-q { flex: 1; min-width: 0; margin: 0; }
.co-finder-head .btn-quiet { min-height: 2.75rem; }
.co-find-list { list-style: none; margin: 0.5rem 0 0; padding: 0; display: grid; gap: 0.25rem; max-height: 22rem; overflow-y: auto; }
.co-find-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 0.15rem 0.6rem; align-items: baseline;
  width: 100%; min-height: 2.75rem; padding: 0.5rem 0.65rem; text-align: left; color: inherit; font: inherit;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer;
}
button.co-find-row:hover, button.co-find-row:focus-visible { border-color: var(--copper-bright); }
.co-find-row--fixed { cursor: default; }
.co-find-what { min-width: 0; overflow-wrap: anywhere; font-weight: 700; }
.co-find-qty, .co-find-total { white-space: nowrap; }
/* One short line, unless two lines share a name — then in full (0083's reason). */
.co-find-desc, .co-find-from {
  grid-column: 1 / -1; min-width: 0; font-size: 0.82rem;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.co-find-desc--full { white-space: normal; overflow: visible; overflow-wrap: anywhere; }
.co-find-none { padding: 0.5rem 0.2rem; }
.co-find-fixed { margin-top: 0.6rem; }
.co-find-fixed summary { cursor: pointer; min-height: 2.75rem; display: flex; align-items: center; }
.co-preview { list-style: none; margin: 0 0 0.9rem; padding: 0; display: grid; gap: 0.6rem; }
.co-prev { padding: 0.75rem 0.85rem; min-width: 0; }
.co-prev--remove { border-left: 3px solid var(--steel); }
.co-prev--add { border-left: 3px solid var(--copper); }
.co-prev-where { margin: 0 0 0.35rem; overflow-wrap: anywhere; }
.co-prev-diff { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.2rem 0.75rem; margin: 0 0 0.4rem; font-size: 0.9rem; }
.co-prev-diff dt { color: var(--muted); }
.co-prev-diff dd { margin: 0; overflow-wrap: anywhere; }
.co-prev-says { margin: 0 0 0.3rem; font-size: 0.9rem; overflow-wrap: anywhere; }
.co-prev-money { margin: 0; font-weight: 700; }
.co-warn { margin: 0 0 0.75rem; padding: 0.6rem 0.85rem 0.6rem 1.8rem; border-color: var(--copper); font-size: 0.9rem; }
.co-final { padding: 0.75rem 0.85rem; margin: 0 0 0.75rem; }
.co-final p { margin: 0 0 0.25rem; }
.co-final-actions, .co-confirm-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.co-final-actions[hidden] { display: none; }
.co-final-note { margin: 0.5rem 0 0.75rem; font-size: 0.85rem; }
.co-confirm { padding: 0.75rem 0.85rem; margin: 0 0 0.75rem; border-color: var(--copper); }
.co-confirm p { margin: 0 0 0.5rem; }

/* The pick-list: the job as it NOW stands, netted over applied change orders.
   Each row is a .ws-box; these rules are only the row's own layout. */
.co-picks { list-style: none; margin: 0 0 0.75rem; padding: 0; display: grid; gap: 0.3rem; }
.co-picks[hidden] { display: none; }
.co-pick { padding: 0; }
/* 0073 — THE WHOLE ROW IS THE TICK. Adam's complaint was that there was "no
   obvious place to tick"; a <label> spanning the row makes every pixel of it
   the target, and the box below is drawn rather than hidden so the target is
   visible as well as large. */
.co-pick-hit {
  display: grid; grid-template-columns: 1.4rem minmax(0, 1fr) 3.5rem 6rem;
  gap: 0.5rem; align-items: center;
  min-height: 2.75rem; padding: 0.55rem 0.7rem; cursor: pointer;
}
.admin .co-pick-hit:hover { background: var(--panel-2); }
/* The real input stays in the DOM for the keyboard and the accessibility tree,
   visually hidden the accessible way — 1px + clip, never display:none — so it
   keeps its place in the tab order. .co-tick is what a person sees.

   ⚠️ SCOPED TO .co-pick-hit ON PURPOSE, and this is the lesson from the build
   that shipped it unscoped. Hiding the native control and drawing a replacement
   means the two MUST arrive together; when a cached older module emitted the
   input without a .co-tick beside it, this rule hid the only checkbox on the
   screen and the pick-list became unusable rather than merely old-looking.
   .co-pick-hit exists only in markup that also emits .co-tick, so any other
   markup — older, newer, or something nobody has written yet — keeps a visible
   native checkbox. A decoration must never be able to remove a control. */
.co-pick-hit .co-pick-box {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* Unmissable AT REST. The 0073 version drew a hairline box on a dark panel and
   Adam's answer was that there was no obvious place to tick — so: a copper edge
   and a lighter well, visible before anything is hovered or checked. */
.co-tick {
  width: 1.25rem; height: 1.25rem; flex: 0 0 auto;
  border: 2px solid var(--copper); background: var(--panel-2);
  position: relative; border-radius: 0;
}
.admin .co-pick-hit:hover .co-tick { background: var(--panel); border-color: var(--copper-bright); }
.co-pick-hit .co-pick-box:checked + .co-tick { background: var(--copper); border-color: var(--copper); }
/* The mark: two borders rotated into a check, so no font or glyph is needed. */
.co-pick-hit .co-pick-box:checked + .co-tick::after {
  content: ""; position: absolute; left: 0.34rem; top: 0.1rem;
  width: 0.3rem; height: 0.6rem; border: solid #14110E;
  border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.co-pick-hit .co-pick-box:focus-visible + .co-tick { outline: 2px solid var(--copper-bright); outline-offset: 2px; }
/* A ticked row is visibly the parent of its editor below. */
.co-pick.is-picked { border-color: var(--copper); }
.co-pick-what { min-width: 0; overflow-wrap: anywhere; }
.co-pick-qty { text-align: right; font-size: 0.9rem; }
.co-pick-total { text-align: right; font-family: var(--font-display); font-weight: 700; color: var(--price); }
.co-pick-from { display: block; padding: 0 0.7rem 0.5rem; font-size: 0.8rem; }
/* 0083 — THE LINE'S DESCRIPTION, so four lines all called "Paint Prep" can be
   told apart before one is ticked. TRUNCATED TO ONE LINE on purpose: the
   pick-list is scanned, and a row that grows to four lines of prose stops being
   scannable. The full text is on the editable copy below, and in the title
   attribute for a hover.

   Outside .co-pick-hit's grid rather than a fifth column in it — a description
   is as long as it is, and giving it a column would either squeeze the money or
   wrap the row. */
.co-pick-desc {
  display: block; padding: 0 0.7rem 0.5rem 2.6rem; margin-top: -0.35rem;
  font-size: 0.82rem; line-height: 1.35;
  /* 0083 followup — IT WRAPS. It used to be nowrap + ellipsis, and the ellipsis
     never rendered: `white-space: nowrap` makes an element's MIN-CONTENT width
     the whole string, and .co-picks is a grid whose items default to
     `min-width: auto` — so instead of truncating, the text won the sizing
     argument and pushed the row past the panel. Measured before the fix in
     headless Chrome: the row 955px wide inside a 736px panel, 241px past its
     right edge at 1100, 478px at narrow widths. Nothing was clipped because
     nothing had agreed to be small.

     ADAM'S CALL, OVERRIDING 0083's TRUNCATION: descriptions wrap to as many
     lines as they need. The scannability argument for one line was real and is
     overruled by the person doing the scanning — he scans BY the descriptions,
     so hiding their ends hides the thing he is reading. The row grows
     vertically, which a list can afford; it cannot afford being wider than its
     container. */
  overflow-wrap: anywhere;
}
/* THE STRUCTURAL HALF, kept even though the wrap alone fixes today's symptom.
   These two lines are what make the failure mode impossible rather than
   currently-absent: a grid item that may shrink, and a row that may shrink
   inside it. The next long unbreakable string put in here — a URL, an account
   number — would otherwise reopen exactly this bug. */
.co-picks { grid-template-columns: minmax(0, 1fr); }
.co-pick { min-width: 0; }
@media (max-width: 34rem) { .co-pick-desc { padding-left: 0.7rem; } }
/* A line the netting can't attach to — legacy change-order rows with no
   line_key, which 0073 deliberately does NOT backfill. .ws-box--muted carries
   the dimming; this is just the note's placement. */
.co-pick--unlinked { padding: 0.55rem 0.7rem; display: grid; gap: 0.25rem; }
.co-pick--unlinked .co-pick-total { text-align: left; }
.co-pick-note { font-size: 0.8rem; }
/* A line with no money on it. Still pickable — "Remove entirely" writes a $0
   removal line that documents the scope change on the addendum — but a
   quantity edit can never register, because the delta engine keys on the
   TOTAL. The row says so rather than letting Adam discover it by being
   silently dropped at save. */
.co-pick-zero { display: block; padding: 0 0.7rem 0.5rem; font-size: 0.8rem; }
/* The tie-back to the picked row above it. */
.co-edit-tie {
  display: inline-block; width: 0.55rem; height: 0.55rem; margin-right: 0.4rem;
  border-left: 2px solid var(--copper); border-bottom: 2px solid var(--copper);
}

/* One picked line, prefilled with what is true NOW. */
/* .ws-box supplies the surface; the copper edge marks it as the child of a
   ticked pick. */
.co-edit { margin: 0 0 0.6rem; padding: 0.7rem 0.8rem; border-left: 3px solid var(--copper); }
.co-edit-what { margin: 0 0 0.5rem; }
.co-edit-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr)); gap: 0.6rem; }
.admin .co-edit input { min-height: 2.75rem; font-size: 1rem; }
/* 0083 — the description field on the editable copy. Full width under the two
   number fields rather than inside their grid: qty and total are short and
   pair naturally, and a textarea in a `minmax(9rem, 1fr)` column would be a
   9rem-wide box for a sentence. */
.co-edit-desc-label { display: block; margin-top: 0.6rem; }
.admin .co-edit-desc {
  width: 100%; min-height: 3.5rem; font-size: 1rem; resize: vertical;
}
.admin .co-edit-desc:disabled { opacity: 0.5; }
.co-edit-whole { margin: 0.5rem 0; }
.co-edit-delta { margin: 0.4rem 0 0; font-size: 0.85rem; }

/* The signed math, live — both halves and the net, the same three figures the
   client is shown, so the two can never be computed differently. */
.co-totals { margin: 0.9rem 0; padding-top: 0.7rem; border-top: 1px solid var(--border); }
.co-totals-list { display: grid; grid-template-columns: 1fr auto; gap: 0.3rem 1rem; margin: 0; }
.co-totals-list dt { color: var(--muted); font-size: 0.9rem; }
.co-totals-list dd { margin: 0; text-align: right; font-size: 0.9rem; }
.co-totals-list .co-net { color: var(--text); font-weight: 700; }
.co-totals-list dd.co-net { font-family: var(--font-display); color: var(--price); }
.co-foot { flex-wrap: wrap; gap: 0.5rem; }

/* Approved but not applied (0070). The same needs-attention treatment the
   schedule board's stale planning uses — copper edge, warn colour, no new
   vocabulary. */
.co-stuck {
  margin: 0 0 0.9rem; padding: 0.75rem 0.85rem;
  background: var(--panel); border: 1px solid var(--copper); border-radius: var(--radius);
}
.co-stuck-head { margin: 0 0 0.3rem; color: var(--copper-bright); font-weight: 700; }
.co-stuck-note { margin: 0 0 0.6rem; font-size: 0.85rem; }
.co-stuck-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.4rem; }
.co-stuck-item { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; flex-wrap: wrap; }

/* The client's addendum (0070) — two sections and a net instead of one list. */
.ev-addendum-lead { margin: 0 0 1rem; }
.ev-section-h {
  margin: 1rem 0 0.4rem; font-family: var(--font-display); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.8rem;
}
.ev-subtotal { margin: 0.4rem 0 0; text-align: right; }
.ev-after { margin: 0.6rem 0 0; }

@media (max-width: 34rem) {
  .co-pick { grid-template-columns: minmax(0, 1fr) auto; }
  .co-pick-qty { grid-column: 1; text-align: left; }
}

/* 0075 — an amendment that is still an ESTIMATE: written, maybe sent, but with
   no change_orders row behind it yet. Dashed edge to say "not issued", against
   the solid cards of the ones that are. */
.co-item--estimate { border-style: dashed; }
.co-item--estimate .co-amount { color: var(--steel); }
/* The line between what is being worked and what is history. */
.co-divider {
  margin: 0.9rem 0 0.5rem; font-family: var(--font-display); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.75rem;
}
.co-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-top: 0.6rem; }
.admin .co-actions .btn { min-height: 2.75rem; font-size: 0.85rem; padding: 0.5rem 0.9rem; border-radius: 0; }

.co-list { list-style: none; padding: 0; margin: 0 0 0.85rem; display: grid; gap: 0.6rem; }
.co-item { padding: 0.8rem 0.9rem; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); }
.co-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.3rem; }
.co-title { color: var(--text); }
.co-facts { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; }
.co-amount { font-family: var(--font-display); font-weight: 700; color: var(--muted); }
.co-amount.is-counted { color: var(--price); }
.co-date, .co-note { font-size: 0.85rem; }
.co-pending { color: var(--copper-bright) !important; }
/* The line under "Contracted" that explains why it grew. */
.pnl-co-note { margin: -0.15rem 0 0.2rem; font-size: 0.78rem; color: var(--copper-bright); }
.pnl-co-note .muted { font-size: 0.78rem; }

/* Estimate builder: the "make this a change order" block. */
.est-co { display: grid; gap: 0.35rem; margin: 0 0 1.25rem; max-width: 34rem; }
.est-co-none { margin: 0; font-size: 0.85rem; }
.est-co-target { display: flex; flex-direction: column; gap: 0.25rem; }
.admin .est-co-target select { min-height: 2.75rem; }

/* --- Client money readout + dashboard profit (Operations 7b) ---
   Reads the 7a engine, so it reuses 7a's colour rule exactly: dollars in the
   price colour, and a NEGATIVE profit in the error red — the same red, in all
   three places it can appear (contract panel, client screens, dashboard), so a
   loss looks identical wherever Adam is standing. */
.client-money { display: flex; align-items: baseline; gap: 0.35rem 0.9rem; flex-wrap: wrap; margin-top: 0.2rem; font-size: 0.85rem; }
.cm-k { color: var(--muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; }
.client-money .is-money { font-family: var(--font-display); font-weight: 700; color: var(--price); }
.client-money .is-loss { font-family: var(--font-display); font-weight: 700; color: #E79A8A; }
.cm-none { font-size: 0.82rem; margin-top: 0.2rem; }

.client-pnl { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 0.75rem 1.25rem; padding: 0.9rem 1rem; background: var(--panel); border: 1px solid var(--border-strong); border-top: 3px solid var(--copper); border-radius: var(--radius); }
.cp-fig { display: grid; gap: 0.1rem; }
.cp-label { margin: 0; font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.68rem; color: var(--muted); }
.cp-todate { text-transform: none; letter-spacing: 0; font-weight: 400; }
.cp-value { margin: 0; font-family: var(--font-display); font-weight: 700; font-size: 1.3rem; }
.cp-value .is-money, .client-pnl .is-money { color: var(--price); }
.cp-value .is-loss, .client-pnl .is-loss { color: #E79A8A; }
.client-pnl-note { margin: 0.5rem 0 0; font-size: 0.82rem; }

/* Dashboard profit plate. The figures sit where the counts do on the Jobs
   plate, but they're money, so they take the money colours. */
.profit-figs .lit-n { font-size: 1.35rem; }
.profit-figs .is-money { color: var(--price); }
.profit-figs .is-loss { color: #E79A8A; }
.profit-note { margin: 0; font-size: 0.8rem; }

/* --- THE JOB DASHBOARD (Jobs reframe, Stage 1) -----------------------------
   One engagement assembled on one screen. See docs/JOBS_REFRAME.md.

   Built entirely from patterns this session's CSS correctness pass established,
   because the dashboard is the first NEW admin surface since it and inheriting
   the old habits would undo the work:
     - auto-fit minmax(0, …) grids, never fixed columns, so nothing has a
       min-content floor to blow the page out at 390px;
     - the long-string rule on anything carrying a name, a reference or a note;
     - 44px controls, and the narrow regime running to 45.99rem so the tablet
       is not styled as a small desktop.
   =========================================================================== */

/* The Dashboard / Details switch. Segmented rather than two links: they are two
   views of one thing, not two destinations. */
.jd-tabs { display: flex; gap: 0.35rem; margin: 0 0 1.25rem; flex-wrap: wrap; }
.admin .jd-tab {
  min-height: 2.75rem; padding: 0.5rem 1rem;
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 0.85rem;
  background: transparent; color: var(--steel);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
}
.admin .jd-tab:hover { color: var(--text); border-color: var(--steel); }
.admin .jd-tab.is-active { background: var(--copper); border-color: var(--copper); color: #14110E; }

/* ============================================================================
   THE WORKSTATION THEME (0072, extracted and named in 0073)
   ============================================================================
   THIS COMMENT IS THE SPEC. A surface adopts the theme by using these classes;
   it does not adopt it by looking similar.

   WHAT IT IS FOR. Screens where Adam is WORKING a record rather than browsing
   one — the job page, the change-order modal, and the pages listed as
   candidates in the 0073 report. It reads as a document: squared off, one trim
   line framing the work area, every category its own box. Not for the
   marketing site, not for the client-facing estimate/signing pages, and not for
   list screens that are genuinely lists.

   THE VOCABULARY, and what goes with what:

     .ws-frame        The page frame. ONE per screen, wrapping the tabs, the
                      action rail and every box. It is what makes a screen read
                      as one instrument rather than stacked lists.
     .ws-box          The unit everything is built from. Squared, bordered,
                      panel background. Use it for any category of content —
                      collapsible or not.
     .ws-box--muted   A box that is present but not actionable (a row that
                      can't be edited, a disabled state). Dimmed and dashed, so
                      it reads as deliberate rather than broken. Pair with
                      aria-disabled on the element.
     .ws-bar-row      The header row of a collapsible box: the bar plus, beside
                      it, an optional bar-resident action.
     .ws-bar          The clickable bar. A real <button>, full width, the WHOLE
                      thing the target. Carries .ws-bar-mark (the chevron),
                      .ws-bar-text (title + note), and optionally
                      .ws-bar-chip (a summary readable while closed).
     .ws-bar-act      An action that sits ON the bar. Only for actions that make
                      sense while the box is CLOSED — see the split documented
                      at the job page's call sites.
     .ws-body         The box's content. Carries all the padding.
     .ws-body-rail    An action rail inside an open body, for actions that need
                      the content in front of you.
     .ws-line-bar     THE ROW-SCALE BAR (0076). Same language as .ws-bar, denser:
                      a line in a list, not a section of a page. Use it where a
                      repeating ROW collapses — the estimate builder's saved
                      lines — rather than stretching .ws-bar, whose 3.25rem
                      minimum and section-weight title are wrong twelve times
                      down a page. Carries .ws-line-mark (the chevron),
                      .ws-line-name (flexible, truncates), fixed-width numeric
                      cells, and .ws-line-chips.
                      ⚠️ A collapsed row must NOT inherit its expanded form's
                      grid — see the note at the rule for how that produced a
                      name stacked one letter per line.

   THE FOUR CONSTRAINTS:

     1. SQUARED CORNERS. border-radius: 0 throughout. This is the one deliberate
        departure from the app's rounded vocabulary and it is what makes a
        workstation read differently from a feed. Buttons inside a themed
        container square off too.
     2. EXISTING TOKENS ONLY. --panel, --panel-2, --border, --border-strong,
        --steel, --muted, --text, --copper. No new colours, no new fonts. The
        palette has three surfaces on purpose; hover lifts the TITLE to
        --copper-bright rather than inventing a fourth.
     3. ⚠️ A DECORATION MUST NEVER BE ABLE TO REMOVE A CONTROL. If a rule hides
        a native control to draw a prettier one, the hide MUST be scoped to
        markup that also emits the replacement — see .co-pick-hit .co-pick-box.
        Unscoped, a stale cached module emitted the input without its drawn
        stand-in and the rule hid the only checkbox on the screen: the form was
        unusable, not merely old-looking.
     4. ⚠️ THE [hidden] GUARD IS PART OF THE THEME, NOT AN AFTERTHOUGHT.
        Any themed container that declares `display` MUST also declare
        `&[hidden] { display: none }`. An author `display` beats the UA's
        [hidden]{display:none}, and this stylesheet has now been bitten by that
        FOUR times — .admin-nav, .set-msg, .modal, and .job-dash in 0071, where
        all three tab panels rendered at once for a whole build because nobody
        wrote the guard. Every rule below that sets display carries it.

   MOBILE (≤34rem): boxes and bars are full-width and stack in document order at
   every size — there is no desktop layout to fall back from. At phone width the
   bar's chip drops under the title (it loses margin-left:auto), a bar-resident
   action goes full width beneath the bar, and the frame and bodies give up most
   of their padding so content keeps the width.
   ========================================================================== */

.ws-frame {
  border: 1px solid var(--border-strong);
  background: var(--panel-2);
  padding: 0.9rem;
  margin: 0 0 1.5rem;
}

.ws-box {
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: 0;
  padding: 0;
  margin: 0;
}
/* Present, not actionable. Dashed and dimmed so it reads as deliberate. */
.ws-box--muted {
  opacity: 0.72;
  border-style: dashed;
  background: transparent;
}

.ws-bar-row { display: flex; align-items: stretch; gap: 0; flex-wrap: wrap; }
.ws-bar-row[hidden] { display: none; }
.admin .ws-bar {
  flex: 1 1 18rem; min-width: 0;
  display: flex; align-items: center; gap: 0.6rem;
  min-height: 3.25rem; padding: 0.7rem 0.95rem; text-align: left;
  background: var(--panel-2); color: var(--text);
  border: 0; border-radius: 0;
  font-family: inherit; font-weight: inherit; text-transform: none; letter-spacing: 0;
  cursor: pointer;
}
.admin .ws-bar[hidden] { display: none; }
.admin .ws-bar:hover { background: var(--panel); }
.admin .ws-bar:hover .ws-bar-title { color: var(--copper-bright); }
.admin .ws-bar:active { background: var(--panel-2); }
.admin .ws-bar:focus-visible { outline: 2px solid var(--copper); outline-offset: -2px; }
.is-open > .ws-bar-row > .ws-bar { border-bottom: 1px solid var(--border-strong); }

/* The chevron. Borders rather than a glyph, so it inherits colour and needs no
   font. Deliberately larger than the 0071 version it replaces — that one was
   the complaint. */
.ws-bar-mark {
  flex: 0 0 auto; width: 0; height: 0;
  border-left: 7px solid var(--copper);
  border-top: 5px solid transparent; border-bottom: 5px solid transparent;
  transition: transform 120ms ease;
}
.is-open .ws-bar-mark { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .ws-bar-mark { transition: none; } }

.ws-bar-text { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; min-width: 0; }
.ws-bar-title {
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.06em; font-size: 0.9rem; color: var(--text);
}
.ws-bar-note { color: var(--muted); font-size: 0.85rem; }
/* The summary you can read WITHOUT opening anything — the reason to collapse. */
.ws-bar-chip {
  margin-left: auto; flex: 0 0 auto;
  padding: 0.25rem 0.55rem; font-size: 0.82rem; color: var(--steel);
  background: var(--panel); border: 1px solid var(--border);
}
.ws-bar-act { display: flex; align-items: center; gap: 0.4rem; padding: 0.5rem 0.6rem; }

/* COLLAPSED MEANS BAR HEIGHT AND NOTHING ELSE. All the padding lives here so no
   closed box can reserve space, and the guard is per constraint 3. */
.ws-body { padding: 0.85rem 0.95rem; }
.ws-body[hidden] { display: none; }
.ws-body-rail { display: flex; gap: 0.4rem; flex-wrap: wrap; margin: 0 0 0.75rem; }
.ws-body-rail[hidden] { display: none; }

/* Squared buttons inside any themed container, so the rail doesn't read as a
   second design language beside the bars. */
.admin .ws-frame .btn, .admin .ws-bar-act .btn, .admin .ws-body-rail .btn { border-radius: 0; }

/* 0074 — SAVE LANDS YOU LOOKING AT IT. A brief copper edge on the thing that
   was just written, so the eye finds the row and not just the section. Colour
   only — no layout property — so nothing moves while the page settles. */
.is-landed {
  outline: 2px solid var(--copper);
  outline-offset: 1px;
  transition: outline-color 400ms ease;
}
@media (prefers-reduced-motion: reduce) { .is-landed { transition: none; } }

/* Sub-heads inside a box — the Contract box holds three things that used to be
   their own <h2>. Quieter than a bar, so they read as parts of one box. */
.ct-sub {
  margin: 1rem 0 0.5rem; font-family: var(--font-display); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.8rem; color: var(--steel);
}
/* The activity section is already a <details> with its own summary — a bar in
   all but name — so it is boxed rather than wrapped in a second disclosure. */
.ct-activity-box { padding: 0.5rem 0.7rem; }

@media (max-width: 34rem) {
  .ws-frame { padding: 0.5rem; }
  .admin .ws-bar { flex: 1 1 100%; padding: 0.7rem 0.75rem; }
  .ws-bar-text { flex: 1 1 100%; }
  .ws-bar-chip { margin-left: 0; }
  .ws-bar-act { flex: 1 1 100%; padding: 0 0.75rem 0.7rem; }
  .admin .ws-bar-act .btn { width: 100%; }
  .ws-body { padding: 0.75rem; }
}

/* --- The job page: the theme's REFERENCE IMPLEMENTATION ---------------------
   0072 built this vocabulary here first; 0073 extracted it above and this is
   now a rename, not a restyle. The job-specific rules that remain are the ones
   that are genuinely about a job page rather than about the theme. */

/* ⚠️ `display: grid` beats [hidden] — constraint 3, and this is the rule that
   taught it: from 0071 until 0072 ALL THREE tab panels rendered at once. */
.job-dash { display: grid; gap: 0.5rem; margin-bottom: 1.25rem; }
.job-dash[hidden] { display: none; }

.jd-tabs { margin: 0 0 0.6rem; }
.admin .jd-tab { border-radius: 0; }
.jd-rail--top {
  display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;
  margin: 0 0 0.9rem; padding-bottom: 0.9rem;
  border-bottom: 1px solid var(--border-strong);
}
.jd-rail--top[hidden] { display: none; }
/* A non-collapsing box still gets the theme's padding. */
.ws-box.jd-section { padding: 0.85rem 0.95rem; }
@media (max-width: 34rem) { .ws-box.jd-section { padding: 0.75rem; } }

/* The stuck-amendments warning is a box too (0072) — same border, squared, so
   it belongs to the frame instead of floating above it. Copper edge because it
   is the one thing on this page that wants looking at. */
.co-stuck { border-radius: 0; border-color: var(--copper); }

/* THE BUY LIST (WO-0079 d) — the engine's list for the bundle-priced lines,
   in its own box beside Materials. A row is one material: name and cost on the
   first line, then what it needs, the packs and the price each — one grid with
   named areas, so the phone layout is a different map of the same cells rather
   than a second markup. ⚠️ min-width: 0 on the cells, for the 0083 reason: one
   long material name must wrap, never push the box wider than the page. */
.bl-view { display: flex; gap: 0.4rem; flex-wrap: wrap; margin: 0 0 0.75rem; }
.bl-view-btn[aria-pressed="true"] { border-color: var(--copper); color: var(--copper-bright); }
.bl-rows { list-style: none; margin: 0 0 0.6rem; padding: 0; }
.bl-row {
  display: grid; gap: 0.15rem 0.75rem; padding: 0.5rem 0; border-bottom: 1px solid var(--border);
  grid-template-columns: minmax(0, 2fr) minmax(0, 1.2fr) minmax(0, 1.6fr) minmax(0, 1.2fr) auto;
  grid-template-areas: "name need packs each cost" "changed changed changed changed changed";
  align-items: baseline;
}
.bl-row > * { min-width: 0; overflow-wrap: anywhere; }
.bl-name { grid-area: name; font-weight: 600; }
.bl-need { grid-area: need; color: var(--muted); }
.bl-packs { grid-area: packs; }
.bl-each { grid-area: each; color: var(--muted); }
.bl-cost { grid-area: cost; text-align: right; font-variant-numeric: tabular-nums; }
.bl-changed { grid-area: changed; color: var(--copper-bright); font-size: 0.9em; }
.bl-row.is-changed .bl-name::after { content: " •"; color: var(--copper-bright); }
.bl-line { margin: 0 0 0.9rem; }
.bl-line-name { margin: 0 0 0.25rem; font-size: 1rem; }
.bl-total, .bl-tax, .bl-trip, .bl-line-cost {
  display: flex; justify-content: space-between; gap: 0.75rem; margin: 0.3rem 0;
}
.bl-total { font-weight: 600; border-top: 1px solid var(--border-strong); padding-top: 0.5rem; }
.bl-tax { color: var(--muted); }
.bl-line-cost { color: var(--muted); }
.bl-fig { font-variant-numeric: tabular-nums; white-space: nowrap; }
.bl-note { font-size: 0.9em; margin: 0.25rem 0 0.6rem; }
.bl-stale { margin-top: 0.9rem; padding: 0.6rem 0.75rem; border-left: 3px solid var(--copper); }
.bl-stale-head { margin: 0 0 0.3rem; }
.bl-stale ul { margin: 0; padding-left: 1.1rem; }
@media (max-width: 34rem) {
  .bl-row {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "name cost" "need need" "packs packs" "each each" "changed changed";
  }
}

/* PHONE (≤34rem). Everything here is already full-width and stacking; these
   rules only stop the bar's three-part row fighting for one line.
   WHAT STACKS: the chip drops under the title+subtitle (it loses its
   margin-left:auto and its own row), and a bar-resident action goes full width
   beneath the bar rather than squeezing beside it. The boxes and bars stay
   edge-to-edge inside the frame, which loses most of its padding so the content
   keeps the width. */
@media (max-width: 34rem) {
  .ct-frame { padding: 0.5rem; }
  .admin .jd-bar { flex: 1 1 100%; padding: 0.7rem 0.75rem; }
  .jd-bar-text { flex: 1 1 100%; }
  .jd-bar-chip { margin-left: 0; }
  .jd-bar-act { flex: 1 1 100%; padding: 0 0.75rem 0.7rem; }
  .admin .jd-bar-act .btn { width: 100%; }
  .jd-body { padding: 0.75rem; }
  .jd-box.jd-section { padding: 0.75rem; }
}

/* The Documents tab — a gathering of papers that already existed, in the
   list vocabulary the peeks and change-order cards use. */
.doc-group {
  margin: 1.1rem 0 0.5rem; font-family: var(--font-display); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.8rem; color: var(--steel);
}
.doc-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.4rem; }
.doc-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.6rem 1rem; flex-wrap: wrap; padding: 0.7rem 0.85rem;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
}
.doc-what { display: grid; gap: 0.15rem; min-width: 0; }
.doc-title { color: var(--text); }
.doc-meta { font-size: 0.85rem; overflow-wrap: anywhere; }
.doc-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.admin .doc-actions .btn { min-height: 2.75rem; font-size: 0.85rem; padding: 0.5rem 0.9rem; }

.jd-section { padding: 0; margin: 0; }
.jd-h {
  margin: 0; font-family: var(--font-display); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.95rem; color: var(--steel);
  display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; min-width: 0;
}
/* Stage 2: heading and its action rail. Wraps rather than shrinking — on a
   phone the buttons drop under the heading and go full width, which is both
   easier to hit and honest about there being no room beside it. */
.jd-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 0.5rem 0.9rem; flex-wrap: wrap; margin: 0 0 0.6rem;
}
.jd-rail { display: flex; gap: 0.4rem; flex-wrap: wrap; }
/* "Which one?" — the in-place chooser a rail action opens when it can't tell
   which invoice or work unit you mean. Its own line, under the button. */
.jd-rail-pick { flex-basis: 100%; display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; margin: 0.4rem 0 0; }
.jd-rail-pick[hidden] { display: none; }
.jd-rail-pick-q { flex-basis: 100%; font-size: 0.85rem; }
/* A rail INSIDE a card (per invoice, per work unit) — separated by a hairline
   so it reads as "act on this row", not as more of the row's content. */
.jd-row-rail {
  display: flex; gap: 0.4rem; flex-wrap: wrap;
  margin-top: 0.55rem; padding-top: 0.5rem; border-top: 1px dashed var(--border-strong);
}
.admin .jd-act { min-height: 2.75rem; font-size: 0.82rem; padding: 0.45rem 0.85rem; }
.admin .jd-act:disabled { opacity: 0.45; cursor: not-allowed; }
.jd-act-msg { display: block; margin: 0 0 0.5rem; }

/* The existing adjustment, shown at the top of the discount dialog above the
   form that would change it. Bordered rather than plain so "what it is now"
   and "what you're about to make it" read as two different things. */
.adj-current {
  margin: 0 0 0.9rem; padding: 0.7rem 0.85rem;
  background: var(--panel); border: 1px solid var(--border);
  border-left: 3px solid var(--copper); border-radius: var(--radius);
}
.adj-current p { margin: 0 0 0.2rem; }
.adj-current p:last-of-type { margin-bottom: 0.5rem; }
.admin #adj-remove { min-height: 2.75rem; }
.jd-h-note { font-size: 0.72rem; letter-spacing: 0.08em; text-transform: none; }
.jd-empty { margin: 0; font-size: 0.9rem; }
.jd-foot { margin: 0; font-size: 0.85rem; }

/* THE JOURNEY. A vertical rail at every width — deliberately not a horizontal
   stepper. Five steps with dates cannot be read across 350px, and a stepper
   that reflows to vertical on a phone is two layouts to keep honest. The rail
   reads the same on a phone and a desktop; only the gutter grows. */
.jd-journey { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.15rem; }
.jd-step { display: grid; grid-template-columns: 1.1rem minmax(0, 1fr); gap: 0.7rem; position: relative; padding: 0.45rem 0; }
/* The connecting line, drawn between dots rather than through them. */
.jd-step:not(:last-child)::before {
  content: ""; position: absolute; left: 0.5rem; top: 1.5rem; bottom: -0.2rem;
  width: 1px; background: var(--border-strong);
}
.jd-step-dot {
  width: 1.1rem; height: 1.1rem; margin-top: 0.15rem; border-radius: 999px;
  border: 2px solid var(--border-strong); background: var(--panel); z-index: 1;
}
.jd-step.is-done .jd-step-dot { background: var(--copper); border-color: var(--copper); }
.jd-step.is-warn .jd-step-dot { background: #E79A8A; border-color: #E79A8A; }
.jd-step-body { display: flex; align-items: baseline; gap: 0.2rem 0.6rem; flex-wrap: wrap; min-width: 0; }
.jd-step-label { color: var(--muted); }
.jd-step.is-done .jd-step-label { color: var(--text); }
.jd-step.is-warn .jd-step-label { color: #E79A8A; }
.jd-step-when { font-family: var(--font-display); font-weight: 700; color: var(--price); }
.jd-step.is-warn .jd-step-when { color: #E79A8A; }
.jd-step-note { font-size: 0.85rem; }

/* THE MONEY. auto-fit so it is four across on a desktop, two on the tablet and
   one on a phone with no breakpoint of its own — minmax(0,…) so a long figure
   can never set a floor wider than the screen. */
.jd-money {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
  gap: 0.75rem;
}
.jd-money--out { margin-top: 0.75rem; }
.jd-money-cell {
  display: grid; gap: 0.1rem; min-width: 0;
  padding: 0.7rem 0.85rem;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
}
.jd-money-cell.is-quiet { opacity: 0.85; }
.jd-money-label {
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.06em; font-size: 0.68rem; color: var(--muted);
}
.jd-money-val { font-family: var(--font-display); font-weight: 700; font-size: 1.25rem; line-height: 1.1; }
.jd-money-note { font-size: 0.72rem; }
.jd-money .is-money { color: var(--price); }
.jd-money .is-loss { color: #E79A8A; }

/* CARDS + ROWS. The card head wraps rather than truncating: a long invoice
   reference or work title belongs on a second line, not behind an ellipsis. */
.jd-list { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.6rem; }
.jd-list--tight { gap: 0.3rem; }
.jd-card {
  min-width: 0;
  padding: 0.8rem 0.9rem;
  background: var(--panel); border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong); border-radius: var(--radius);
}
.jd-card.is-dim { opacity: 0.6; }
.jd-card-head { display: flex; align-items: baseline; gap: 0.4rem 0.6rem; flex-wrap: wrap; min-width: 0; }
.jd-card-title { color: var(--text); font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
.jd-card-kind { font-size: 0.85rem; }
/* Pushed right when there is room, and simply next in the wrap order when there
   isn't — no second layout to maintain. */
.jd-card-amount { margin-left: auto; font-family: var(--font-display); font-weight: 700; }
.jd-card-meta { margin: 0.3rem 0 0; font-size: 0.85rem; min-width: 0; overflow-wrap: anywhere; }
.jd-card-note { margin: 0.4rem 0 0; font-size: 0.9rem; color: var(--text); overflow-wrap: anywhere; }
.admin a.jd-link {
  display: inline-flex; align-items: center; min-height: 2.75rem;
  margin-top: 0.2rem; font-size: 0.85rem; color: var(--copper); text-decoration: none;
}
.admin a.jd-link:hover { color: var(--copper-bright); text-decoration: underline; }

.jd-sublist { list-style: none; margin: 0.5rem 0 0; padding: 0.5rem 0 0; display: grid; gap: 0.2rem; border-top: 1px dashed var(--border-strong); }
.jd-subrow { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: baseline; gap: 0.6rem; font-size: 0.88rem; }
.jd-subrow--boxed { padding: 0.55rem 0.85rem; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); }
.jd-sub-when { font-size: 0.8rem; white-space: nowrap; }
.jd-sub-what { min-width: 0; overflow-wrap: anywhere; }
.jd-sub-amount { font-family: var(--font-display); font-weight: 700; }
.jd-total { margin: 0.5rem 0 0; text-align: right; font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); }
.jd-total strong { font-size: 1.05rem; margin-left: 0.4rem; }

/* WHAT THE SUB HAS BEEN PAID, on the work card itself. The receipts used to
   live only in the cost drawer, which is collapsed and loaded on demand — so
   from the dashboard a recorded payment left no mark and got recorded twice.
   No new row shape: the payments reuse .jd-subrow, which already has the
   minmax(0,1fr) track, the long-string rule and the 45.99rem stacking. */
.jd-subpay { margin: 0.6rem 0 0; }
.jd-subpay-none { margin: 0; font-size: 0.85rem; }
.jd-subpay-list { margin: 0; }

/* The one place this surface needs a width rule of its own. The sub-row's date
   column is dead weight on a phone: it forces the description into a third of
   the row. Same 45.99rem the rest of the admin's narrow rules use. */
@media (max-width: 45.99rem) {
  .jd-subrow { grid-template-columns: minmax(0, 1fr) auto; row-gap: 0.1rem; }
  .jd-sub-when { grid-column: 1 / -1; }
  /* Full-width action buttons on a narrow screen: two half-width buttons side
     by side are the shape that gets mis-tapped, and nothing else is competing
     for the row. */
  .jd-rail, .jd-row-rail { flex-direction: column; align-items: stretch; }
  .admin .jd-act { width: 100%; }
}

/* --- P&L panel + job costs (Operations 7a) ---
   The one rule here: a NEGATIVE profit is the error colour, never the price
   colour. Every other dollar on the screen is copper-ish and reads as neutral
   information; a contract underwater has to look different from one that isn't,
   or it reads as just another number. */
.pnl {
  display: grid; gap: 0.9rem; padding: 1rem 1.1rem; margin: 0 0 1.25rem;
  background: var(--panel); border: 1px solid var(--border-strong);
  border-top: 3px solid var(--copper); border-radius: var(--radius);
}
.pnl-void { margin: 0; font-size: 0.88rem; }
.pnl-missing { margin: 0 0 1.25rem; }
.pnl-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0.9rem 1.5rem; }
.pnl-col { margin: 0; display: grid; gap: 0.3rem; align-content: start; }
.pnl-head {
  margin: 0 0 0.15rem; font-family: var(--font-display); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.7rem; color: var(--copper-bright);
}
.pnl-row { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.pnl-row dt { color: var(--muted); font-size: 0.9rem; }
.pnl-row dd { margin: 0; font-family: var(--font-display); font-weight: 700; font-size: 1rem; }
.pnl-row.is-quiet dt, .pnl-row.is-quiet dd { opacity: 0.8; }
.pnl .is-money { color: var(--price); }
/* Underwater. Same red as the admin's danger button border. */
.pnl .is-loss { color: #E79A8A; }

.pnl-profits { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 0.75rem; padding-top: 0.85rem; border-top: 1px solid var(--border); }
.pnl-profit { display: grid; gap: 0.15rem; }
.pnl-profit-label {
  margin: 0; font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.06em; font-size: 0.75rem; color: var(--steel);
}
.pnl-todate { text-transform: none; letter-spacing: 0; font-weight: 400; color: var(--muted); }
.pnl-profit-value { margin: 0; font-family: var(--font-display); font-weight: 700; font-size: 1.75rem; line-height: 1.05; }
.pnl-profit-note { margin: 0.1rem 0 0; font-size: 0.78rem; color: var(--muted); }

/* --- Per-job cost drawer --- */
.job-costs { margin-top: 0.6rem; }
/* 0173 — 44px, not 40: this toggle is the only way to a receipt's "Correct"
   (and to Record payment / Remove), and a real press at phone width refused it. */
.admin .job-costs-toggle { min-height: var(--ws-tap); font-size: 0.82rem; }
.job-costs-open { display: grid; gap: 0.75rem; padding-top: 0.65rem; border-top: 1px dashed var(--border-strong); }
.job-costs-summary { display: flex; align-items: baseline; gap: 0.4rem 1rem; flex-wrap: wrap; font-size: 0.88rem; color: var(--muted); }
.job-costs-summary strong { font-family: var(--font-display); font-size: 1rem; }
.job-costs-summary .is-money { color: var(--price); }
.job-costs-summary .is-over { color: var(--copper-bright); }
.cost-block { display: grid; gap: 0.4rem; }
.cost-head { margin: 0; font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.7rem; color: var(--steel); }
.cost-empty { margin: 0; font-size: 0.85rem; }
.cost-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.25rem; }
/* The last track carries the delete control and matches its 2.75rem width. */
.cost-row { display: grid; grid-template-columns: auto 1fr auto 2.75rem; align-items: baseline; gap: 0.6rem; padding: 0.3rem 0; border-bottom: 1px solid var(--border); }
.cost-row:last-child { border-bottom: 0; }
.cost-amount { font-family: var(--font-display); font-weight: 700; color: var(--price); }
.cost-meta { font-size: 0.85rem; color: var(--text); min-width: 0; overflow-wrap: anywhere; }
.cost-date { font-size: 0.8rem; white-space: nowrap; }
/* 2.75rem, not the 2rem this was. The old rule set min-height:0 at (0,2,1),
   which out-specifies `.admin button { min-height: 2.75rem }` at (0,1,1) — so
   the one control on this row that deletes something was also the smallest
   thing to hit, 32px at every width. Fixed by giving this selector the right
   number rather than by !important; the grid track below widens to match.
   align-self overrides the row's baseline alignment: a 44px box on a baseline
   sits low and drags the row taller. */
.admin .cost-del {
  width: 2.75rem; height: 2.75rem; min-height: 2.75rem; align-self: center;
  padding: 0; font-size: 1.1rem; line-height: 1;
  background: transparent; color: var(--muted); border: 1px solid transparent; border-radius: var(--radius);
}
.admin .cost-del:hover { color: #E79A8A; border-color: #5B1E12; }
.admin .cost-add { min-height: 2.5rem; font-size: 0.82rem; padding: 0.45rem 0.75rem; justify-self: start; }

/* An invoice's payments list (0154 refunds). Amount and what-it-was on one
   line; receipt state, refund state and the reason on their own lines; the
   buttons in a wrapping row underneath, so a card payment's three of them
   never crowd the amount at 390px. */
.cost-row.pay-row { grid-template-columns: auto 1fr; row-gap: 0.25rem; }
.pay-row > .cost-meta:not(:nth-child(2)), .pay-row .pay-actions, .pay-row .set-msg { grid-column: 1 / -1; }
.pay-row .pay-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.pay-row.is-refund .cost-amount { color: #E79A8A; }
.pay-row .pay-why { color: var(--muted); }
.pay-row .pay-refunded { color: var(--copper-bright); }

/* The refund dialog, drawn in the confirm host. */
.refund-form { display: grid; gap: 0.6rem; width: 100%; }
.refund-form p { margin: 0; }
.rf-facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.5rem; margin: 0; }
.rf-facts dt { font-size: 0.75rem; color: var(--muted); }
.rf-facts dd { margin: 0; font-family: var(--font-display); font-weight: 700; }
.rf-facts .rf-left { color: var(--price); }
.rf-prior { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.25rem; font-size: 0.85rem; overflow-wrap: anywhere; }
.rf-field { display: grid; gap: 0.25rem; font-size: 0.9rem; }
.rf-field input, .rf-field select, .rf-field textarea { width: 100%; box-sizing: border-box; min-height: 2.75rem; }
.rf-field .rf-max { font-size: 0.8rem; }
.rf-msg[hidden] { display: none; }
/* Money may have moved: amber and calm, never the red "that didn't work". */
.rf-msg.rf-warn {
  padding: 0.7rem 0.9rem; background: var(--panel);
  border: 1px solid #6E5B34; border-left: 3px solid #E7C98A; border-radius: var(--radius);
}
.rf-warn .rf-warn-head { display: block; color: #E7C98A; margin-bottom: 0.25rem; }
@media (max-width: 45.99rem) {
  .cost-row.pay-row { grid-template-columns: auto 1fr; }
}

@media (max-width: 45.99rem) {
  .cost-row { grid-template-columns: auto 1fr 2.75rem; row-gap: 0.15rem; }
  .cost-date { grid-column: 2 / 3; }
}

/* --- Jobs, on a contract (Operations 6a) ---
   Reuses the invoice card's action rail and message slot (.inv-card-actions,
   .inv-card-msg) — same component, different payload — so a job row and an
   invoice row behave identically under the thumb. */
.ct-jobs { display: grid; gap: 0.85rem; margin-bottom: 1.5rem; }
.job-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.6rem; }
.job-card {
  padding: 0.8rem 0.9rem;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
}
/* A cancelled job stays on the page — it's history — but stops competing. */
.job-card.is-cancelled { opacity: 0.6; }
.job-card-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.35rem; }
.job-title { color: var(--text); }
.job-on-cal { font-size: 0.75rem; color: var(--muted); white-space: nowrap; }
/* 0091 — same fact as .sch-line, on the job page's card. It sits in the head
   row beside the status badge, so it is shaped as a chip rather than a line of
   prose; the border is the hairline the workstation theme uses everywhere a
   thing is named rather than actioned. A long line name wraps rather than
   pushing the badge off the row. */
.job-line {
  font-size: 0.75rem;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.05rem 0.5rem;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* The line picker in the Add Work dialog. The separator says the picker feeds
   the fields under it rather than being one more field beside them. */
.jf-pick { padding-bottom: 0.6rem; border-bottom: 1px solid var(--border); }
.jf-pick-note { margin: 0.35rem 0 0; font-size: 0.85rem; }
.jf-line-was { margin: 0 0 0.2rem; font-size: 0.85rem; }
.job-card-facts { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; }
.job-who { color: var(--text); font-size: 0.9rem; }
.job-pay { font-family: var(--font-display); font-weight: 700; color: var(--price); }
/* 0169 — paid over the agreed figure, and the agreed pay's history: what it
   was, what it is, and why. Office only. */
.job-over { font-size: 0.85rem; font-weight: 600; color: var(--copper-bright); }
.jd-paychange-list { margin: 0.5rem 0 0; }
.jd-paychange .jd-sub-what,
.jd-paycorrection .jd-sub-what { overflow-wrap: anywhere; }
.jd-sub-by { font-size: 0.8rem; }
/* 0173 — a recorded payment's corrections, under its receipt: what it WAS,
   and why. Spans the row's grid; the receipt itself shows what it IS. */
.jd-paycorr-list { grid-column: 1 / -1; margin: 0.15rem 0 0; padding: 0; list-style: none; font-size: 0.8rem; }
.jd-paycorr { overflow-wrap: anywhere; }
.cpx-lead { margin: 0 0 0.5rem; font-size: 0.9rem; }
.cpx-note { margin: -0.25rem 0 0.5rem; font-size: 0.85rem; color: var(--copper, var(--muted)); }
.job-when { font-size: 0.85rem; color: var(--muted); }
.job-notes { margin: 0.45rem 0 0; font-size: 0.88rem; color: var(--muted); white-space: pre-wrap; }

/* Planning against an unsigned contract is allowed and normal — this states
   the fact in the copper "attention" register, not the red "error" one. */
.ct-unsigned-note {
  margin: 0; padding: 0.6rem 0.75rem; font-size: 0.88rem; color: #E7C98A;
  background: rgba(196, 99, 44, 0.1); border: 1px solid var(--copper); border-radius: var(--radius);
}

/* Checkbox rows in the Operations dialogs (notify-contractor / notify-client).
   44px target, label and control on one line, text allowed to wrap under it. */
.inv-check { display: flex; align-items: flex-start; gap: 0.6rem; min-height: 2.75rem; padding: 0.35rem 0; cursor: pointer; }
/* 1.5rem: the LABEL was already a 44px target (min-height above), but the box
   itself was 18px — big enough to see, small enough to miss when the thing you
   are confirming is "email the client". The label stays the tap area; this is
   the box catching up. */
.admin .inv-check input[type="checkbox"] { width: 1.5rem; height: 1.5rem; min-height: 0; margin-top: 0.05rem; flex: none; accent-color: var(--copper); }
.inv-check span { font-size: 0.92rem; color: var(--text); }
.inv-check .muted { font-size: 0.85rem; }

/* WO-0078 — who goes on a site visit: the card's Assigned to line, its choice,
   the visit form's Assign to, and the name on a Scheduled row. A radio group
   of .inv-check labels, so every option is a 44px target. */
.assign-pick { border: 0; margin: 0.25rem 0 0; padding: 0; min-width: 0; }
.assign-pick:disabled { opacity: 0.6; }
.admin .inv-check input[type="radio"] { width: 1.5rem; height: 1.5rem; min-height: 0; margin-top: 0.05rem; flex: none; accent-color: var(--copper); }
.lead-assign { margin: 0.5rem 0; }
.lead-assign-line { margin: 0; }
.lead-assign-label { color: var(--muted); font-size: 0.85rem; margin-right: 0.35rem; }
.lead-row-assignee { display: block; font-size: 0.8rem; color: var(--muted); }

/* --- Contractors (Operations 5) ---
   Deliberately thin: the page reuses the Clients tab's list/detail vocabulary
   wholesale (.client-list, .client-fields, .client-detail-actions), because it
   IS the same shape of screen and a second set of near-identical classes would
   only be a second thing to keep in step. These are the two places it differs:
   the count line carries the inactive toggle, and the detail actions can hold
   an inline message under them. */
.contractors .clients-count { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem 1rem; flex-wrap: wrap; }
.admin .contractors .clients-count .btn-quiet { min-height: 2.5rem; font-size: 0.85rem; }
.contractors .client-detail-actions .set-msg { flex-basis: 100%; }
.contractor-retired { margin: 0.9rem 0 0; font-size: 0.9rem; }

/* --- Invoicing (Operations 4a) ---
   Two screens share this vocabulary: the top-level Invoices tab and the billing
   block on a contract. Same classes on both so an invoice never looks like two
   different things depending on where you're standing.
   Money reads in --price; only lateness gets a warm warning colour. */
.inv-page-note { margin: -0.5rem 0 1rem; font-size: 0.9rem; }

.inv-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem 1rem; flex-wrap: wrap; margin-bottom: 0.85rem; }
.inv-owed { margin: 0; color: var(--muted); }
.inv-owed strong { font-family: var(--font-display); font-weight: 700; font-size: 1.35rem; color: var(--price); margin-left: 0.35rem; }
.inv-filters { display: flex; gap: 0.35rem; flex-wrap: wrap; }
.admin .inv-filter {
  min-height: 2.5rem; padding: 0.4rem 0.75rem; font-size: 0.85rem;
  background: transparent; color: var(--steel);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
}
.admin .inv-filter:hover { color: var(--text); border-color: var(--steel); }
.admin .inv-filter.is-active { color: #14110E; background: var(--copper); border-color: var(--copper); }
.inv-filter-n { opacity: 0.7; margin-left: 0.2rem; }

.inv-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.6rem; }
/* A row is a STACK: the [content][buttons] line, then anything that belongs
   under it — the inline confirm, the per-row message, the payments list, the
   "Sent —" line.

   It used to be the flex ROW itself, with all six of those as direct children.
   Only the first two are columns; the other four became columns too, and
   align-items:stretch pulled them to full row height. So an invoice with no
   payment rendered a tall EMPTY box beside its buttons (the .inv-payments
   wrapper, which paymentsHtml fills with "" in that case), and "Sent — Aug 22"
   and "Payments & receipts" floated off to the right as their own columns
   instead of sitting under the row.
   
   Three of those four children were added later — the row was a two-child flex
   line when it was written, and each addition looked local and correct. */
.inv-item { display: flex; flex-direction: column; gap: 0.5rem; }
.inv-item-main { display: flex; align-items: stretch; gap: 0.5rem; }
/* An empty payments wrapper is still a flex item: zero height, but it collects
   a 0.5rem gap for a box nobody can see. :empty is exactly this case — the div
   has to stay in the markup because paintPayments repaints into it. */
.inv-payments:empty { display: none; }
.admin .inv-open {
  display: flex; flex-direction: column; gap: 0.3rem; flex: 1 1 auto; min-width: 0;
  text-align: left; padding: 0.8rem 0.9rem; min-height: 2.75rem;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer;
  font-family: var(--font-body); font-weight: 400; text-transform: none; letter-spacing: 0;
}
.admin .inv-open:hover { border-color: var(--steel); }
/* Overdue earns a copper edge — the one thing on this screen that's late. */
.inv-item.is-overdue .inv-open { border-left: 3px solid var(--copper); }
/* The invoice just arrived at via ?invoice= — "Invoice directly" on an estimate
   lands here, and in a long list the new row is otherwise indistinguishable.
   An outline rather than a border swap, so it doesn't fight the overdue rule
   above when a row is both. */
.inv-item.is-highlight .inv-open { outline: 2px solid var(--copper); outline-offset: 2px; }
/* --- The invoice sheet (0125) -------------------------------------------- */
/* One ws-modal for every invoice. Sections stack; the facts and the money are
   small two-column grids that fold to one column on a narrow phone. */
.inv-sheet { display: grid; gap: 1rem; }
.inv-sheet-sec { display: grid; gap: 0.4rem; min-width: 0; }
.inv-sheet-sec + .inv-sheet-sec { border-top: 1px solid var(--border); padding-top: 0.9rem; }
.inv-sheet-h {
  margin: 0; font-family: var(--font-display); font-size: 0.82rem;
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted);
}
.inv-sheet-client { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; font-size: 1.05rem; overflow-wrap: anywhere; }
.inv-sheet-sub { margin: 0; color: var(--muted); }
.inv-sheet-facts, .inv-sheet-money {
  margin: 0; display: grid; gap: 0.35rem 1rem;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
}
.inv-sheet-facts div, .inv-sheet-money div { min-width: 0; }
.inv-sheet-facts dt, .inv-sheet-money dt { font-size: 0.78rem; color: var(--muted); }
.inv-sheet-facts dd, .inv-sheet-money dd { margin: 0; overflow-wrap: anywhere; }
.inv-sheet-money dd { font-weight: 600; color: var(--price); }
.inv-sheet-money .is-clear dd { color: var(--muted); }
/* ⚠️ pre-wrap, AND THE TEXT IS ESCAPED BEFORE IT GETS HERE. The notes on an
   "Invoice directly" invoice are a whole estimate rendered as lines; keeping
   the newlines is what makes them readable, and escaping is what makes them
   safe. overflow-wrap so a long unbroken line can't widen the sheet. */
.inv-sheet-notes {
  margin: 0; white-space: pre-wrap; overflow-wrap: anywhere;
  padding: 0.6rem 0.75rem; background: var(--panel);
  border: 1px solid var(--border); border-radius: var(--radius);
  font-size: 0.95rem; line-height: 1.45;
}
.inv-sheet-links { margin: 0; display: flex; flex-wrap: wrap; gap: 0.25rem 1rem; }
/* Links to a record, sized as targets: they are the only ways off the sheet. */
.inv-sheet-link {
  display: inline-flex; align-items: center; min-height: 2.75rem;
  color: var(--copper); font-weight: 600;
}
.inv-sheet-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
.inv-sheet-actions:empty { display: none; }
.inv-sheet-hint { margin: 0; font-size: 0.85rem; }
.admin #is-edit-form input, .admin #is-edit-form textarea { font-size: 1rem; min-height: 2.75rem; }

.inv-row-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.inv-row-sub, .inv-row-money { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; }
.inv-num { font-family: var(--font-display); font-weight: 600; letter-spacing: 0.04em; font-size: 0.85rem; color: var(--steel); }
.inv-kind { font-size: 0.85rem; color: var(--muted); }
.inv-due { font-size: 0.85rem; color: var(--muted); }
.inv-due.is-overdue { color: var(--copper-bright); }
.inv-amount { font-family: var(--font-display); font-weight: 700; color: var(--price); }
.inv-paid { font-size: 0.85rem; color: var(--muted); }
.inv-balance { font-family: var(--font-display); font-weight: 600; font-size: 0.9rem; color: var(--copper-bright); }
.inv-balance.is-clear { color: #9FD8B0; }
.inv-row-actions { display: flex; align-items: center; flex: none; }
.admin .inv-row-actions .btn { min-height: 2.75rem; font-size: 0.85rem; padding: 0.5rem 0.85rem; white-space: nowrap; }

.pay-modal-sub { margin: -0.4rem 0 0.9rem; font-size: 0.9rem; }

/* --- Invoices & Billing, on a contract --- */
.ct-billing { display: grid; gap: 0.85rem; margin-bottom: 1.5rem; }
.ct-rollup {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 0.5rem 1rem;
  margin: 0; padding: 0.85rem 0.95rem;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
}
.ct-rollup.is-over { border-color: var(--copper); }
.ct-rollup > div { display: flex; flex-direction: column; gap: 0.1rem; }
.ct-rollup dt { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.68rem; color: var(--muted); }
.ct-rollup dd { margin: 0; font-family: var(--font-display); font-weight: 700; font-size: 1.15rem; color: var(--text); }
.ct-rollup dd.is-money { color: var(--price); }
.ct-rollup dd.is-over { color: var(--copper-bright); }
/* Step 6: a split of Invoiced, not a figure of its own — so it reads quieter. */
.ct-rollup .ct-rollup-draft dd { color: var(--muted); font-weight: 600; }
/* Step 7: a final-balance invoice whose figure the job has moved away from. */
.inv-stale {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem;
  margin: 0.5rem 0; padding: 0.55rem 0.7rem; font-size: 0.88rem; color: #E7C98A;
  background: rgba(196, 99, 44, 0.1); border: 1px solid var(--copper); border-radius: var(--radius);
}
.inv-stale p { margin: 0; flex: 1 1 16rem; }
/* A send that didn't go (estimate send, Option A): the record is unchanged, so
   this line is the only place it shows. Warm, not alarming — nothing reached
   the client. */
.send-failed {
  margin: 0.35rem 0 0; padding: 0.45rem 0.65rem; font-size: 0.86rem; color: #E7C98A;
  background: rgba(196, 99, 44, 0.1); border: 1px solid var(--copper); border-radius: var(--radius);
}
.inv-stale strong { color: var(--copper-bright); }
.ct-over-warn { margin: 0; padding: 0.6rem 0.75rem; font-size: 0.88rem; color: #E7C98A; background: rgba(196, 99, 44, 0.1); border: 1px solid var(--copper); border-radius: var(--radius); }
.ct-over-warn strong { color: var(--copper-bright); }
.ct-billing-actions { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.admin .ct-billing-actions .btn { min-height: 2.75rem; font-size: 0.9rem; padding: 0.5rem 1rem; }

.inv-card {
  display: block; padding: 0.8rem 0.9rem;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
}
.inv-card.is-overdue { border-left: 3px solid var(--copper); }
.inv-card-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.3rem; }
.inv-card-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-top: 0.6rem; }
.admin .inv-card-actions .btn { min-height: 2.5rem; font-size: 0.82rem; padding: 0.45rem 0.75rem; }
.inv-card-msg { display: block; margin-top: 0.5rem; }

@media (max-width: 45.99rem) {
  /* .inv-item is a column at every width now; it's the [content][buttons] line
     inside it that stacks on a phone. */
  .inv-item-main { flex-direction: column; }
  .admin .inv-row-actions .btn { width: 100%; }
}

/* ===========================================================================
   THE FORGE — /admin/forging/ dashboard.

   The one admin screen built to look forged rather than utilitarian. All of it
   comes from the existing tokens: charcoal/steel surfaces, hairline seams,
   copper for accents and heat, --price for money. Flat and solid — the warmth
   is copper, not glow. Sub-tabs (clients/estimates/contracts) deliberately keep
   the plain admin styling; only this landing gets the treatment.
   =========================================================================== */

/* A "steel plate": flat charcoal face, hairline seam, four rivets. */
.plate {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
}
.rivet {
  position: absolute; width: 5px; height: 5px; border-radius: 50%;
  background: var(--border-strong);
  box-shadow: inset 0 1px 0 rgba(236, 230, 221, 0.25);
}
.rivet-tl { top: 6px; left: 6px; }
.rivet-tr { top: 6px; right: 6px; }
.rivet-bl { bottom: 6px; left: 6px; }
.rivet-br { bottom: 6px; right: 6px; }

.forge { display: grid; gap: 1.5rem; }

/* --- Masthead --- */
.forge-masthead {
  display: flex; align-items: center; gap: 1.1rem; flex-wrap: wrap;
  padding: 1.4rem 1.5rem;
  border-bottom: 2px solid var(--copper);
}
.forge-masthead-mark { display: flex; align-items: flex-end; gap: 0.15rem; color: var(--steel); flex: none; }
.forge-masthead-mark .forge-hammer { color: var(--copper); margin-bottom: 0.4rem; }
.forge-masthead-text { min-width: 0; }
.forge-eyebrow {
  margin: 0; font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.14em; font-size: 0.7rem; color: var(--copper-bright);
}
.forge-title {
  margin: 0.1rem 0 0.2rem; font-family: var(--font-display); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em; font-size: 2rem; line-height: 1;
  color: var(--text);
}
.forge-sub { margin: 0; color: var(--muted); font-size: 0.9rem; }

/* --- Stat plates --- */
.stat-plates { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); gap: 0.75rem; }
.stat-plate { padding: 1rem 1.1rem; }
.stat-label {
  margin: 0 0 0.35rem; font-family: var(--font-display); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.72rem; color: var(--copper-bright);
}
.stat-value {
  margin: 0; font-family: var(--font-display); font-weight: 700; font-size: 1.9rem;
  line-height: 1.05; color: var(--text);
}
.stat-value.is-money { color: var(--price); }
.stat-sub { margin: 0.3rem 0 0; color: var(--muted); font-size: 0.8rem; }

/* --- Quick actions --- */
/* 0071 — the amendment tiles. Two plates side by side in the dashboard's
   existing plate/rivet vocabulary; the warn state borrows the copper edge the
   schedule board's stale planning already uses, so nothing new is invented. */
.amd-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr)); gap: 0.75rem; }
.amd-tile { padding: 0.9rem 1rem; }
.amd-tile.is-stuck { border-color: var(--copper); }
.amd-rows { display: grid; gap: 0.3rem; margin-top: 0.6rem; }
.admin a.amd-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.6rem;
  min-height: 2.75rem; padding: 0.5rem 0.6rem;
  background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text); text-decoration: none; font-size: 0.9rem;
}
.admin a.amd-row:hover { border-color: var(--steel); }
.amd-who { min-width: 0; overflow-wrap: anywhere; }
.amd-amt { font-family: var(--font-display); font-weight: 700; color: var(--price); }

.forge-actions { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.admin .forge-actions .btn { min-height: 2.75rem; font-size: 0.9rem; padding: 0.5rem 1.1rem; text-decoration: none; }
.forge-cold-note { margin: 0; font-size: 0.9rem; }

/* --- Sections --- */
.forge-section { display: grid; gap: 0.75rem; }
.forge-h {
  margin: 0; font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.06em; font-size: 1.05rem; color: var(--steel);
  display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap;
}
.forge-h-note { font-size: 0.75rem; letter-spacing: 0.1em; color: var(--muted); }

/* --- The heat line: cold stock (steel) → forged (copper) ---
   Each stage carries its own accent, walking the same steel→copper ramp as the
   bar above it, so the metaphor reads even with colour alone stripped out (the
   counts and labels do the real work). */
.heat-line { display: grid; gap: 0.6rem; }
.heat-bar {
  height: 4px; border-radius: 2px; position: relative;
  background: linear-gradient(90deg, var(--border-strong) 0%, var(--steel) 30%, var(--copper) 72%, var(--copper-bright) 100%);
}
/* The ember sits on the hot end. Static by default; it only breathes for users
   who haven't asked for reduced motion. */
.heat-ember {
  position: absolute; right: -2px; top: -3px; width: 10px; height: 10px; border-radius: 50%;
  background: var(--copper-bright);
}
.heat-stages { display: grid; grid-template-columns: 1fr auto 1fr auto 1fr auto 1fr; align-items: stretch; gap: 0.4rem; }
.admin a.heat-stage {
  display: flex; flex-direction: column; gap: 0.1rem; justify-content: center;
  min-height: 2.75rem; padding: 0.8rem 0.6rem; text-decoration: none;
  background: var(--panel); border: 1px solid var(--border);
  border-top: 3px solid var(--border-strong); border-radius: var(--radius);
  color: var(--text);
}
.admin a.heat-stage:hover { border-color: var(--steel); }
.heat-stage.is-heat-0 { border-top-color: var(--border-strong); }
.heat-stage.is-heat-1 { border-top-color: var(--steel); }
.heat-stage.is-heat-2 { border-top-color: var(--copper); }
.heat-stage.is-heat-3 { border-top-color: var(--copper-bright); background: var(--panel-2); }
.heat-count { font-family: var(--font-display); font-weight: 700; font-size: 1.6rem; line-height: 1; color: var(--text); }
.heat-stage.is-heat-3 .heat-count { color: var(--copper-bright); }
.heat-label { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.8rem; color: var(--steel); }
.heat-note { font-size: 0.75rem; color: var(--muted); }
.heat-arrow { align-self: center; color: var(--border-strong); font-size: 1.1rem; }

/* --- Activity ledger --- */
.ledger-plate { padding: 0.9rem 1rem; }
.ledger { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.15rem; }
.ledger-row {
  display: grid; grid-template-columns: 1.4rem 1fr auto auto auto;
  align-items: center; gap: 0.6rem;
  padding: 0.55rem 0.2rem; border-bottom: 1px solid var(--border);
}
.ledger-row:last-child { border-bottom: 0; }

/* A LINKED ledger row. The row's grid moves onto the <a>, because the anchor
   becomes the row's only child and would otherwise collapse the columns.
   Rows without a record id keep the plain .ledger-row above — same layout,
   no link. */
.ledger-row.is-link { display: block; padding: 0; }
.admin a.ledger-link {
  display: grid; grid-template-columns: 1.4rem 1fr auto auto auto;
  align-items: center; gap: 0.6rem;
  padding: 0.55rem 0.5rem; margin: 0 -0.3rem;
  color: inherit; text-decoration: none; border-radius: var(--radius);
  /* Covered by the global prefers-reduced-motion: reduce rule at the end of
     this file, which kills all transitions. */
  transition: background-color 120ms ease, box-shadow 120ms ease;
}
/* Forge vocabulary, kept quiet: the surface lifts to the lighter plate and a
   copper hairline strikes down the left edge. No glow, no movement. */
.admin a.ledger-link:hover {
  background: var(--panel-2);
  box-shadow: inset 3px 0 0 var(--copper);
  color: inherit;
}
.admin a.ledger-link:focus-visible { background: var(--panel-2); }
/* The label is the thing being opened, so it takes the copper on hover — the
   money and timestamp stay put. */
.admin a.ledger-link:hover .ledger-label { color: var(--copper-bright); }
.ledger-kind { display: inline-flex; color: var(--steel); }
.ledger-contract { color: var(--copper-bright); }
.ledger-label { color: var(--text); min-width: 0; overflow-wrap: anywhere; }
.ledger-total { font-family: var(--font-display); font-weight: 700; color: var(--price); }
.ledger-time { color: var(--muted); font-size: 0.8rem; white-space: nowrap; }

/* --- Cold on the anvil: unlit plates, no data --- */
.cold-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); gap: 0.75rem; }
.cold-tile { padding: 1rem 1.1rem; background: var(--bg); border-style: dashed; opacity: 0.75; }
.cold-name { margin: 0 0 0.25rem; font-family: var(--font-display); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; font-size: 1rem; color: var(--steel); }
.cold-note { margin: 0 0 0.6rem; color: var(--muted); font-size: 0.85rem; }
.cold-tag { margin: 0; font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.68rem; color: var(--border-strong); }

/* --- A LIT plate: a cold tile that now carries real numbers (4a invoicing,
   6a jobs). Solid face, copper top edge. When it's an <a> the whole plate is
   the target, not a caption under it; `.is-static` is the same plate for a
   panel with nowhere to go yet. */
.admin .forge-lit {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
  padding: 1rem 1.1rem; text-decoration: none; color: var(--text);
  border-top: 3px solid var(--copper);
}
.admin a.forge-lit:hover { border-color: var(--steel); border-top-color: var(--copper-bright); }
.lit-main { min-width: 0; }
.lit-side { display: flex; gap: 1.5rem; flex-wrap: wrap; }
.lit-fig { display: flex; flex-direction: column; gap: 0.1rem; text-align: right; }
.lit-n { font-family: var(--font-display); font-weight: 700; font-size: 1.6rem; line-height: 1; color: var(--steel); }
.lit-cap { font-size: 0.75rem; color: var(--muted); }
/* Late money is the one number on this plate allowed to shout. */
.lit-fig.is-late .lit-n { color: var(--copper-bright); }
.lit-fig.is-late .lit-cap { color: var(--copper-bright); }

/* Work actually happening gets the copper; a zero stays quiet steel. */
.stat-value.is-hot { color: var(--copper-bright); }

@media (max-width: 45.99rem) {
  .admin .forge-lit { flex-direction: column; align-items: flex-start; }
  .lit-fig { text-align: left; }
}

/* Stack the line on a phone: stages full width, arrows rotated to point down. */
@media (max-width: 45.99rem) {
  .heat-stages { grid-template-columns: 1fr; }
  .heat-arrow { transform: rotate(90deg); justify-self: center; }
  .forge-title { font-size: 1.6rem; }
  .ledger-row, .admin a.ledger-link { grid-template-columns: 1.4rem 1fr auto; row-gap: 0.2rem; }
  .ledger-total, .ledger-time { grid-column: 2 / -1; }
}

/* Motion is opt-in, never the default. */
@media (prefers-reduced-motion: no-preference) {
  .heat-ember { animation: forge-ember 3.2s ease-in-out infinite; }
  @keyframes forge-ember {
    0%, 100% { opacity: 0.55; box-shadow: 0 0 0 0 rgba(217, 138, 69, 0); }
    50% { opacity: 1; box-shadow: 0 0 0 4px rgba(217, 138, 69, 0.18); }
  }
}

/* The copper gateway into the workspace, in the main admin nav. */
.admin-nav-forge {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.06em; font-size: 0.85rem;
  padding: 0.5rem 0.9rem; min-height: 2.5rem;
  background: var(--copper); color: #14110E; text-decoration: none;
  border: 1px solid var(--copper); border-radius: var(--radius);
}
.admin-nav-forge:hover { background: var(--copper-bright); border-color: var(--copper-bright); color: #14110E; }
.admin-nav-forge .forge-mark { flex: none; }

/* The workspace's own nav: same bones as admin-nav, copper seam to signal
   you've stepped into the Forge. */
.forging-nav { border-bottom-color: var(--copper); }
.forging-nav-back {
  display: inline-flex; align-items: center; min-height: 2.5rem;
  padding: 0.5rem 0.7rem 0.5rem 0;
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 0.8rem; color: var(--muted); text-decoration: none;
  margin-right: 0.35rem; border-right: 1px solid var(--border); padding-right: 0.8rem;
}
.forging-nav-back:hover { color: var(--text); }

/* ===========================================================================
   Client contract signing (/sign-contract/) — STOREFRONT, not admin. A
   homeowner opens this from an email, often on a phone.
   =========================================================================== */
.sign-h { font-size: 1.15rem; margin: 2rem 0 0.75rem; padding-bottom: 0.4rem; border-bottom: 1px solid var(--border); }
/* The agreement itself: scrollable so the signature block stays reachable
   without a page of scrolling first, but tall enough to actually read. */
.sign-terms {
  max-height: 26rem; overflow-y: auto;
  padding: 1.1rem 1.25rem; margin-bottom: 0.5rem;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
}
.sign-terms p { color: var(--muted); margin: 0 0 0.75rem; }
.sign-terms p:last-child { margin-bottom: 0; }
.sign-term-h { font-size: 1rem; color: var(--text); margin: 1.1rem 0 0.4rem; }
.sign-term-h:first-child { margin-top: 0; }

.sign-lines { list-style: none; padding: 0; margin: 0 0 1rem; display: grid; gap: 0.5rem; }
.sign-line {
  display: grid; grid-template-columns: 3.5rem 1fr 6rem 6rem; gap: 0.5rem; align-items: baseline;
  padding: 0.7rem 0.8rem;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
}
.sign-line-qty { color: var(--muted); }
.sign-line-desc { color: var(--text); }
.sign-line-unit { color: var(--muted); font-size: 0.9rem; text-align: right; }
.sign-line-total { font-family: var(--font-display); font-weight: 700; color: var(--price); text-align: right; }
.sign-total { font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); text-align: right; margin: 0 0 1rem; }
.sign-total strong { color: var(--price); font-size: 1.3rem; margin-left: 0.5rem; }

/* Client-facing contract notes (0034) — small muted print below the totals. */
.sign-notes { color: var(--muted); font-size: 0.85rem; max-width: 40rem; margin: 0 0 1.25rem; }
.sign-notes-h { font-size: 0.9rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em; margin: 0 0 0.4rem; }
.sign-notes p { margin: 0 0 0.4rem; }

.sign-form { max-width: 34rem; }
.sign-form input[type="text"] { min-height: 3rem; font-size: 1rem; }
.sign-agree { display: flex; align-items: flex-start; gap: 0.75rem; margin: 1rem 0 1.25rem; color: var(--muted); }
.sign-agree input { flex: none; width: 1.35rem; height: 1.35rem; }
.sign-form .btn { min-height: 3rem; }
.sign-form .btn:disabled { opacity: 0.5; cursor: not-allowed; }

@media (max-width: 34rem) {
  .sign-line { grid-template-columns: 3rem 1fr; }
  .sign-line-unit, .sign-line-total { text-align: left; grid-column: 2; }
}

@media (max-width: 34rem) {
  .ro-line { grid-template-columns: 3rem 1fr; }
  .ro-unit, .ro-total { text-align: left; grid-column: 2; }
}

/* ===========================================================================
   THE LINE EDITOR'S LAYOUT (0077) — one block, every child, three widths
   ===========================================================================
   The open .est-line is a form of up to FIFTEEN children, and which of them
   exist depends on the caller. This block is the ONLY place any of them is
   placed. If you add a sixteenth, it goes in the four tables below and nowhere
   else — that is the whole point of gathering them here.

   ⚠️ 0105 ADDED A CHILD AND DID NOT DO THAT, which is this law being broken by
   the build after the one that wrote it. The materials disclosure got an
   `order` and a `flex: 1 1 100%` — a FLEXBOX declaration, inert on a grid item
   — and no grid-column in any regime. So it auto-placed into whatever cell was
   free: 56px in the builder's ten-track desktop grid, 92px in the change-order
   modal's, half a phone. Adam sent a screenshot of a textarea wrapping at three
   characters. The fix is four lines, one per regime, and they are below.

   ⚠️ EVERY CHILD GETS A COLUMN IN EVERY REGIME. Not "every field": the crew
   strip, "Save to library", "Remove" and "Done" are children too, and leaving
   them out is how 0077 was reported. A grid child with no placement is
   auto-placed into whatever cell is free, which on the builder's desktop grid
   meant the crew strip landed in the 3.5rem "#" column with "Assign Crew"
   wrapped to two lines inside it, and "Done" orphaned in the next free cell
   bottom-left. Same family as 0076's collapsed-row squish: a child inherited a
   grid nobody had placed it in.

   ⚠️ AND AN ORDER. `order` defaults to 0, so an unlisted child sorts BEFORE
   every listed one. That is why the crew strip and "Done" rendered at the TOP
   of the editor — above the "#" field — at both widths under 46rem. The order
   list below is complete and shared by all three regimes, so there is one
   reading order to reason about rather than one per breakpoint.

   THE FOUR CALLERS AND WHAT THEY RENDER. Each row below was read off the
   editableLinesHtml() call at the line named, and the two classes come from
   exactly two options: `--costed` from showInHouse, `--ordered` from
   showPosition (admin-lines.js lineRowHtml, the return statement).

     estimate builder      admin-estimates.js:1716     --costed --ordered
       all sixteen children: the only caller passing crewFor and isOpen, so the
       crew strip and the collapse bar exist here and nowhere else.
     contract editor       admin-contracts.js:5502     --ordered
       the "#" and the photos. No in-house, so no Save-to-library either — one
       flag gates both.
     change-order modal    admin-change-order.js:114   --costed
       everything the builder has except the "#", the crew strip and "Done".
     service call editor   admin-service-call.js:114   --costed
       the same set as the change-order modal (0106).

   ⚠️ THIS LIST WAS WRONG FROM 0086 TO 0107 AND IS WHY check:grid EXISTS. It
   said three callers and gave the contract editor a bare `.est-line` — true
   when 0077 wrote it, and false from 0086, which gave a draft contract's lines
   the reordering field and so the --ordered class with it. A law nothing reads
   drifts from the code it governs, quietly, in the file people go to in order
   to find out what is true. scripts/check-line-grid.mjs now reads the same
   block and fails the build when it disagrees with the stylesheet; this
   paragraph is the part no checker can verify, so it names its own source.

   The variants are kept mutually exclusive by `--ordered` / `--costed` so no
   rule here can depend on source order to win — see 0074's note about the
   contract rules silently overriding the builder's at identical specificity.

   ⚠️ AN OPTIONAL CHILD IS PLACED LAST IN ITS ROW, OR ALONE ON ONE. Explicit
   columns mean an absent child leaves a HOLE, and a hole at the right-hand end
   of a row is invisible while a hole in the middle is a bug report. In-house is
   the last cell of the money row; the "#", the photos, the crew strip and
   "Done" are alone or at an end. Put a new optional child in the same position
   or give it its own row.

   THE SHAPE, at every width — three bands:
     IDENTITY   #, item name, description, materials
     MONEY      price · qty · discount · total · in-house, then non-taxable
     FOOTER     photos strip · crew strip · action row
   The footer is the region 0077 is about. It was not a region before: photos,
   crew and the three buttons were five loose children competing for cells with
   the money fields. Now they are the last three rows at every width, in that
   order, and the action row is Save-to-library · Remove … Done, with Done at
   the far end because it is the exit.

   MEASURED (Chrome, headless, the full costed builder line, card 1048px):
     before   crew strip 56px wide with a 3px dotted rule, Save-to-library
              376px, Remove floating mid-row, Done 56px bottom-left
     after    see the four-width table in the 0077 commit message. */

/* --- ONE READING ORDER, ALL WIDTHS ---------------------------------------
   Matches DOM order except for In-House, which moves from between Qty and
   Discount to after Total so the money row reads as the arithmetic it is:
   price × qty − discount = total, and then, quietly, what it costs Adam.
   The trade is that tab order still runs through In-House before Discount.
   Accepted for the same reason the photo control's did: this is one
   admin-only field, and it is discoverability at stake, not the typing flow. */
/* 0 rather than unlisted: an unplaced child ALSO sorts at 0, and the difference
   between "first on purpose" and "0077's bug again" has to be visible in the
   list itself. The header is the row's own bar — it comes before the fields it
   opens onto, at every width. */
.est-line .el-line-head { order: 0; }
.est-line .el-f-pos     { order: 1; }
.est-line .el-f-name    { order: 2; }
.est-line .el-f-desc    { order: 3; }
/* WO-0079 — THE BUNDLE STRIP sits directly under Description, which on a
   bundle line IS the answers ("Fence: 120 linear ft · Gate: 1"): what the line
   is, then what priced it and how to change that. Everything below moved down
   one so the list stays contiguous (check:grid). */
.est-line .el-bundle    { order: 4; }
/* 0105 followup — MATERIALS JOINS THE IDENTITY BAND, directly under
   Description. It was order 11, down in the footer beside the photo strip,
   which read as an afterthought bolted to the row's controls. It is the same
   species of field as Description — prose about the work, full width — and the
   two get written in one sitting: what the work is, then what it takes. */
.est-line .el-f-materials { order: 5; }
.est-line .el-f-price   { order: 6; }
.est-line .el-f-qty     { order: 7; }
.est-line .el-f-disc    { order: 8; }
.est-line .el-f-total   { order: 9; }
.est-line .el-f-inhouse { order: 10; }
.est-line .el-f-notax   { order: 11; }
.est-line .el-f-img     { order: 12; }
.est-line .el-crew      { order: 13; }
.est-line .el-tolib     { order: 14; }
.est-line .el-remove    { order: 15; }
.est-line .el-collapse  { order: 16; }

/* WO-0079 — THE BUNDLE STRIP. One full-width child of a bundle line: the chip
   that says what priced it, the sentence that says why the money is locked,
   and the ways to change it. min-width: 0 for the same reason as the materials
   disclosure below — a long bundle name must not widen the row. */
.el-bundle {
  min-width: 0; display: grid; gap: 0.45rem;
  padding: 0.6rem 0.75rem; border: 1px solid var(--copper); border-left-width: 3px;
  border-radius: var(--radius); background: var(--panel-2);
}
.el-bundle p { margin: 0; }
.el-bundle-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.6rem; }
.el-bundle-chip { color: var(--text); overflow-wrap: anywhere; }
.el-bundle-said { font-size: 0.85rem; }
.el-bundle-overridden { font-size: 0.9rem; color: var(--copper-bright); overflow-wrap: anywhere; }
.el-bundle-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.admin .el-bundle .btn-quiet { min-height: 2.75rem; }
.el-bundle-ovr { display: grid; gap: 0.5rem; grid-template-columns: minmax(0, 9rem) minmax(0, 1fr); align-items: end; }
.el-bundle-ovr-actions { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 0.4rem; }
.el-bundle-ovr-msg { grid-column: 1 / -1; }
.el-bundle-problem {
  display: grid; gap: 0.4rem; padding: 0.55rem 0.7rem;
  border: 1px solid #8A4234; border-radius: var(--radius); background: rgba(138, 66, 52, 0.12);
}
.el-bundle-problem-text { color: #E79A8A; overflow-wrap: anywhere; }
.el-bundle-problem-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.el-bundle-cost-sum { cursor: pointer; color: var(--muted); font-size: 0.9rem; }
.el-bundle-cost-sum .is-money { color: var(--text); }
.el-bundle-cost-sum .is-loss { color: #E79A8A; }
.el-bundle-buys { list-style: none; margin: 0.4rem 0 0; padding: 0; display: grid; gap: 0.35rem; }
.el-bundle-buy {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0.1rem 0.6rem;
  padding: 0.35rem 0; border-top: 1px solid var(--border); font-size: 0.9rem;
}
.el-bundle-buy-name { overflow-wrap: anywhere; }
.el-bundle-buy-qty { grid-column: 1 / 2; color: var(--muted); }
.el-bundle-buy-cost { grid-column: 2 / 3; grid-row: 1 / 3; align-self: center; }
.el-bundle-flag { grid-column: 1 / -1; color: var(--copper-bright); font-size: 0.85rem; }
.el-bundle-cost-note { font-size: 0.8rem; margin-top: 0.35rem; }
.el-chip-bundle { border-color: var(--copper); color: var(--copper-bright); }
/* The locked money fields SAY they are locked (the strip's sentence); this only
   stops them reading as a broken input. */
.admin .est-line--bundle input:disabled, .admin .est-line--bundle select:disabled { opacity: 0.7; cursor: not-allowed; }
/* The dialog. Its two steps toggle with [hidden], and .client-form's display
   would otherwise outrank the attribute and draw the hidden step (the
   display-vs-[hidden] trap) — so the attribute is made to win here. */
#bundle-line-modal [hidden] { display: none; }
.bl-bundles { display: grid; gap: 0.5rem; }
.bl-choice {
  display: flex; justify-content: space-between; gap: 0.75rem; width: 100%; min-height: 2.75rem;
  padding: 0.6rem 0.8rem; text-align: left; color: var(--text); background: var(--panel-2);
  border: 1px solid var(--border-strong); border-radius: var(--radius); cursor: pointer;
}
.bl-choice:hover, .bl-choice:focus-visible { border-color: var(--copper); }
.bl-choice-name { overflow-wrap: anywhere; }
.bl-which { margin: 0 0 0.5rem; display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4rem; }
.bl-questions { display: grid; gap: 0.6rem; }
.bl-q-sq { border: 1px solid var(--border); border-radius: var(--radius); padding: 0.5rem 0.7rem; margin: 0; display: grid; gap: 0.45rem; min-width: 0; }
.bl-modes { display: flex; gap: 1rem; }
.bl-modes label { display: inline-flex; align-items: center; gap: 0.35rem; min-height: 2.75rem; }
.bl-sides { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.5rem; }
.bl-sides[hidden], .bl-area[hidden] { display: none; }
.bl-q-job { margin: 0; }
.bl-preview { margin-top: 0.75rem; padding-top: 0.6rem; border-top: 1px solid var(--border); }
.bl-preview p { margin: 0.2rem 0; overflow-wrap: anywhere; }
.bl-line-name { font-weight: 600; }
.bl-total strong { font-size: 1.15rem; }
.bl-minimum { margin: 0.25rem 0 0; font-weight: 700; font-size: 0.9rem; } /* WO-0102 */
.bl-warnings { margin: 0.35rem 0 0; padding-left: 1.1rem; color: var(--copper-bright); font-size: 0.9rem; }

/* THE MATERIALS DISCLOSURE. Quiet by construction — it is a note to self on a
   screen full of money, and it must not compete with the figures beside it.

   ⚠️ min-width: 0 IS LOAD-BEARING (the 0083 followup). A grid item's default
   min-width is auto, which refuses to shrink below its content's min-content
   width — so one unbroken string in the textarea would push the whole line row
   wider than its card and hand the editor a horizontal scrollbar. The same
   declaration, for the same reason, as .modal-panel's. */
.el-materials { width: 100%; min-width: 0; }
.el-materials-sum {
  cursor: pointer; list-style: none;
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.2rem 0; color: var(--muted);
  font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.06em;
}
.el-materials-sum::-webkit-details-marker { display: none; }
.el-materials-sum::before {
  content: "▸"; display: inline-block; font-size: 0.7rem;
  transition: transform 0.15s ease;
}
.el-materials[open] > .el-materials-sum::before { transform: rotate(90deg); }
.el-materials-sum:hover { color: var(--text); }
/* THE FIELD ITSELF TAKES DESCRIPTION'S TREATMENT, deliberately and to the
   declaration: same font-size, same line-height, same padding, same resize, and
   a min-height of three rows against Description's one. Two textareas holding
   the same kind of prose, a centimetre apart, that did not match would read as
   two different kinds of field.

   min-width: 0 again — a textarea is a replaced element with its own intrinsic
   width (the `cols` default), and without this it sets the grid track's floor
   rather than following it. */
.admin .est-line textarea.el-materials-text {
  width: 100%; min-width: 0; min-height: 4.5rem; margin-top: 0.3rem;
  font-family: inherit; font-size: 1rem; line-height: 1.4;
  padding: 0.5rem 0.6rem; resize: vertical;
}
@media (prefers-reduced-motion: reduce) {
  .el-materials-sum::before { transition: none; }
}

/* --- THE ACTION ROW'S THREE WEIGHTS --------------------------------------
   Three buttons that are NOT peers, wearing one .btn-quiet between them. The
   markup is shared with three callers and byte-frozen, so the hierarchy is
   drawn here rather than by handing one of them a different class.
     Done            the exit, and the only one with a consequence Adam wants
                     — a copper edge at rest, filled on hover. Never .btn-danger
                     weight: closing a row is not destructive.
     Remove          quiet at rest, destructive only under the cursor.
     Save to library a utility. Smaller and shorter than the other two, and
                     never stretched to fill its cell. */
/* --- THE OPEN ROW'S HEADER (0098) ----------------------------------------
   .ws-line-bar's own dress does the work — that is the point, and it is why
   this is the component rather than something shaped like it. Three overrides:

   THE COLUMNS. The collapsed bar carries five (mark, #, name, qty, total); the
   header carries three, because qty and total are two fields away being edited
   and a second copy of them at the top would be a number to distrust. Without
   this the name would stop 11rem short of the row's edge.

   THE BLEED. -0.8rem top and -0.9rem right take the header to the row's own
   border on both edges, which is what makes the chevron land at the SAME y as
   the collapsed row's (that row has no padding; this one has 0.8rem). The left
   margin stays 0: the gutter is the drag handle's and the header starts where
   the collapsed bar starts. Measured, not assumed — mark at 50.2px from the
   row's left edge in both states at 1200px.

   THE RULE UNDER IT. One hairline, so the header reads as chrome on the form
   rather than as the form's first field. */
.admin .est-line .el-line-head {
  grid-template-columns: 0.9rem 2rem minmax(0, 1fr);
  margin: -0.8rem -0.9rem 0.35rem 0;
  border-bottom: 1px solid var(--border);
}

.admin .est-line .el-collapse {
  min-height: 2.75rem; color: var(--text); border-color: var(--copper);
}
.admin .est-line .el-collapse:hover {
  background: var(--copper); border-color: var(--copper); color: #14110E;
}
.admin .est-line .el-remove { min-height: 2.75rem; }
.admin .est-line .el-remove:hover { color: #E79A8A; border-color: #8A4234; }
.admin .est-line .el-tolib { min-height: 2.5rem; font-size: 0.85rem; }
/* justify-self, not width: a grid item stretches to its cell by default, which
   is the whole story of the 376px "Save to library" in the report. */
.est-line .el-tolib, .est-line .el-remove { justify-self: start; }

/* -- DESKTOP AND WIDER (≥46rem) -------------------------------------------
   TEN equal columns behind the builder's narrow "#", so the money fields pair
   up two tracks each and the action row has cells near its buttons' own width
   rather than a fifth of the card. The card itself never exceeds .container's
   68rem, so this regime is also the 2500px case in the report: at any viewport
   past ~1130px the line is the same 1048px wide and lays out identically. */
@media (min-width: 46rem) {
  .est-line          { grid-template-columns: repeat(10, minmax(0, 1fr)); align-items: end; }
  .est-line--ordered { grid-template-columns: 3.5rem repeat(10, minmax(0, 1fr)); }

  /* THE BUILDER — the "#" is a left rail; everything it numbers starts at the
     second line, and the full-width bands cross both.
       #  | Item name
       Description
       Materials
          | Price | Qty | Discount | Total | In-House
          | Non-taxable
       Photos
       Crew
          | Save to library | Remove | ......................... | Done        */
  .est-line--ordered .el-line-head { grid-column: 1 / -1; }
  .est-line--ordered .el-f-pos     { grid-column: 1 / 2; }
  .est-line--ordered .el-f-name    { grid-column: 2 / -1; }
  .est-line--ordered .el-f-desc    { grid-column: 1 / -1; }
  .est-line--ordered .el-bundle    { grid-column: 1 / -1; }
  .est-line--ordered .el-f-materials { grid-column: 1 / -1; }
  .est-line--ordered .el-f-price   { grid-column: 2 / 4; }
  .est-line--ordered .el-f-qty     { grid-column: 4 / 6; }
  .est-line--ordered .el-f-disc    { grid-column: 6 / 8; }
  .est-line--ordered .el-f-total   { grid-column: 8 / 10; }
  .est-line--ordered .el-f-inhouse { grid-column: 10 / -1; }
  .est-line--ordered .el-f-notax   { grid-column: 2 / 6; }
  .est-line--ordered .el-f-img     { grid-column: 1 / -1; }
  .est-line--ordered .el-crew      { grid-column: 1 / -1; }
  .est-line--ordered .el-tolib     { grid-column: 2 / 4; }
  .est-line--ordered .el-remove    { grid-column: 4 / 6; }
  .est-line--ordered .el-collapse  { grid-column: 10 / -1; justify-self: end; }

  /* THE CONTRACT EDITOR AND THE CHANGE-ORDER MODAL — no "#", so the same shape
     shifted one rail left. Both are placed here; the only thing the change
     order adds is In-House and Save-to-library, and both were MISSING from
     this half before 0077 — the modal's two extra children auto-placed. */
  .est-line:not(.est-line--ordered) .el-line-head { grid-column: 1 / -1; }
  .est-line:not(.est-line--ordered) .el-f-name    { grid-column: 1 / -1; }
  .est-line:not(.est-line--ordered) .el-f-desc    { grid-column: 1 / -1; }
  .est-line:not(.est-line--ordered) .el-bundle    { grid-column: 1 / -1; }
  .est-line:not(.est-line--ordered) .el-f-materials { grid-column: 1 / -1; }
  .est-line:not(.est-line--ordered) .el-f-price   { grid-column: 1 / 3; }
  .est-line:not(.est-line--ordered) .el-f-qty     { grid-column: 3 / 5; }
  .est-line:not(.est-line--ordered) .el-f-disc    { grid-column: 5 / 7; }
  .est-line:not(.est-line--ordered) .el-f-total   { grid-column: 7 / 9; }
  .est-line:not(.est-line--ordered) .el-f-inhouse { grid-column: 9 / -1; }
  .est-line:not(.est-line--ordered) .el-f-notax   { grid-column: 1 / 5; }
  .est-line:not(.est-line--ordered) .el-f-img     { grid-column: 1 / -1; }
  .est-line:not(.est-line--ordered) .el-crew      { grid-column: 1 / -1; }
  .est-line:not(.est-line--ordered) .el-remove    { grid-column: 1 / 3; }
  .est-line:not(.est-line--ordered) .el-collapse  { grid-column: 9 / -1; justify-self: end; }
  /* Save-to-library exists exactly when In-House does — both come from the one
     showInHouse flag — so --costed is the honest test for whether Remove leads
     the action row or follows it. */
  .est-line--costed:not(.est-line--ordered) .el-tolib  { grid-column: 1 / 3; }
  .est-line--costed:not(.est-line--ordered) .el-remove { grid-column: 3 / 5; }

  /* `.admin .est-line input` (0,2,1) sets font-size:1rem and would outrank a
     bare `.el-f-name input` (0,1,1) — specificity beats source order, so the
     size silently would not apply while the weight did. Matching that
     selector's depth so the rule does what it says. */
  .admin .est-line .el-f-name input { font-size: 1.05rem; font-weight: 600; }
}

/* -- TABLET (34–46rem) ----------------------------------------------------
   SIX columns, so the money still pairs up two tracks at a time and reads as
   three-up rather than as a stack. A 700px tablet in portrait had no layout at
   all before 0074 and a two-column one after it; six tracks is the same idea
   with room for the footer's action row to stay a row.
       #  | Item name
       Description
       Materials
       Price | Qty | Discount
       Total | In-House
       Non-taxable
       Photos
       Crew
       Save to library | Remove | ................. | Done                    */
@media (min-width: 34rem) and (max-width: 45.99rem) {
  .est-line { grid-template-columns: repeat(6, minmax(0, 1fr)); align-items: end; }

  /* The "#" shares row one here as it does at desktop. Six tracks is what
     makes that affordable — the name keeps five of them, where the earlier
     two-column layout would have halved a client-facing label to sit beside a
     two-digit number, which is the trade it rightly refused. */
  .est-line .el-line-head { grid-column: 1 / -1; }
  .est-line .el-f-pos     { grid-column: 1 / 2; }
  .est-line .el-f-name    { grid-column: 1 / -1; }
  .est-line--ordered .el-f-name { grid-column: 2 / -1; }
  .est-line .el-f-desc    { grid-column: 1 / -1; }
  .est-line .el-bundle    { grid-column: 1 / -1; }
  .est-line .el-f-materials { grid-column: 1 / -1; }
  .est-line .el-f-price   { grid-column: 1 / 3; }
  .est-line .el-f-qty     { grid-column: 3 / 5; }
  .est-line .el-f-disc    { grid-column: 5 / -1; }
  .est-line .el-f-total   { grid-column: 1 / 3; }
  .est-line .el-f-inhouse { grid-column: 3 / 5; }
  .est-line .el-f-notax   { grid-column: 1 / 4; }
  .est-line .el-f-img     { grid-column: 1 / -1; }
  .est-line .el-crew      { grid-column: 1 / -1; }
  .est-line .el-remove    { grid-column: 1 / 3; }
  .est-line .el-collapse  { grid-column: 5 / -1; justify-self: end; }
  .est-line--costed .el-tolib  { grid-column: 1 / 3; }
  .est-line--costed .el-remove { grid-column: 3 / 5; }
}

/* -- PHONE (<34rem) -------------------------------------------------------
   TWO columns, not one. A single column was the old answer and it is what put
   Total four fields and 300px below Price: a phone user opens a line to check
   that the math is right, and the three numbers that make it have to be in
   view together. Text fields still take the full width — a name or a
   description at 154px is not worth the tidiness.
       #
       Item name
       Description
       Materials
       Price | Qty
       Discount | Total
       In-House
       Non-taxable
       Photos
       Crew
       Save to library | Remove
       Done (full width — the exit, and the easiest thing to hit)             */
@media (max-width: 33.99rem) {
  .est-line { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .est-line .el-line-head { grid-column: 1 / -1; }
  .est-line .el-f-pos     { grid-column: 1 / -1; }
  .est-line .el-f-name    { grid-column: 1 / -1; }
  .est-line .el-f-desc    { grid-column: 1 / -1; }
  .est-line .el-bundle    { grid-column: 1 / -1; }
  .est-line .el-f-materials { grid-column: 1 / -1; }
  .est-line .el-f-price   { grid-column: 1 / 2; }
  .est-line .el-f-qty     { grid-column: 2 / -1; }
  .est-line .el-f-disc    { grid-column: 1 / 2; }
  .est-line .el-f-total   { grid-column: 2 / -1; }
  .est-line .el-f-inhouse { grid-column: 1 / -1; }
  .est-line .el-f-notax   { grid-column: 1 / -1; }
  .est-line .el-f-img     { grid-column: 1 / -1; }
  .est-line .el-crew      { grid-column: 1 / -1; }
  .est-line .el-remove    { grid-column: 1 / 2; }
  .est-line .el-collapse  { grid-column: 1 / -1; justify-self: stretch; }
  .est-line--costed .el-tolib  { grid-column: 1 / 2; }
  .est-line--costed .el-remove { grid-column: 2 / -1; }

  /* The %/$ toggle gives up its comfortable width so the discount control fits
     a half-width cell — 4.25rem + 5rem of value field wants 154px and the cell
     is 154px on a 390px phone. Still shows the symbol and the chevron. */
  .est-line .el-disc-type { width: 3.6rem; }
  .est-line .el-disc-val { min-width: 4rem; }
}

/* Its pair lives beside .est-client-fields — see the note there for why this
   one boundary stays at 30rem while the rest of the admin's narrow rules sit
   at 45.99rem. Move one and you must move the other. */
@media (max-width: 30rem) {
  .client-fields { grid-template-columns: 1fr; gap: 0.15rem; }
  .client-fields dd { margin-bottom: 0.5rem; }
}

.board { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 1rem; align-items: start; }
.board-col { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 0.75rem; }
.board-col h3 { font-size: 0.95rem; margin-bottom: 0.6rem; color: var(--steel); }
.board-col .count { color: var(--muted); font-weight: 400; }
.admin .booking-card {
  display: flex; flex-direction: column; gap: 0.15rem; width: 100%; text-align: left;
  margin-bottom: 0.5rem; padding: 0.6rem 0.7rem;
  background: var(--panel-2); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--radius); cursor: pointer;
  font-family: var(--font-body); font-weight: 400; text-transform: none; letter-spacing: 0; min-height: auto;
}
.admin .booking-card:hover { border-color: var(--steel); }
.booking-card .bc-name { font-weight: 600; }
.booking-card .bc-size, .booking-card .bc-dates { font-size: 0.85rem; color: var(--muted); }
.booking-card .bc-price { font-family: var(--font-display); font-weight: 700; color: var(--price); }

.detail-panel {
  position: fixed; left: 1rem; right: 1rem; bottom: 1rem; margin: 0 auto; max-width: 46rem;
  background: var(--panel-2); border: 1px solid var(--border-strong); border-radius: var(--radius);
  padding: 1.5rem 1.6rem; max-height: 86vh; overflow-y: auto; z-index: 60;
}
.detail-head {
  display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem;
  padding-bottom: 1rem; border-bottom: 1px solid var(--border);
}
.detail-head-main { display: flex; flex-direction: column; gap: 0.5rem; min-width: 0; }
.detail-head h3 { font-size: 1.25rem; margin: 0; line-height: 1.2; }
.detail-badges { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
.badge-status { color: var(--text); border-color: var(--steel); }
.detail-source { color: var(--muted); font-size: 0.85rem; }
.detail-hold {
  margin: 0.9rem 0 0; color: var(--steel); font-size: 0.9rem;
}
.detail-hold [data-countdown] { font-family: var(--font-display); font-weight: 600; color: var(--copper-bright); }

/* Titled sections with real separation — a hairline rule + vertical rhythm. */
.detail-section { padding: 1.25rem 0; border-top: 1px solid var(--border); }
.detail-head + .detail-section, .detail-hold + .detail-section { border-top: 0; }
.detail-sec-h {
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 0.8rem; color: var(--steel);
  margin: 0 0 0.85rem;
}

/* THE JOB — comfortable two-column key/value on desktop, one column on mobile. */
.detail-kv { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem 1.75rem; }
.detail-field { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.detail-field--wide { grid-column: 1 / -1; }
.detail-field-label { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.02em; font-size: 0.75rem; color: var(--muted); }
.detail-field-val { color: var(--text); line-height: 1.5; word-break: break-word; }
.detail-field-val a { color: var(--copper-bright); }
.detail-field-val.detail-price { font-family: var(--font-display); font-weight: 700; color: var(--price); font-size: 1.1rem; }

.detail-notes { min-height: 5.5rem; }
.detail-actions { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center; }
.detail-actions--tight { margin-top: 0.6rem; }
.detail-inline-msg { margin-top: 0.8rem; }
.pickup-inline { display: inline-flex; gap: 0.4rem; align-items: center; }
.pickup-inline input[type="date"] { width: auto; min-height: 2.5rem; }

/* --- Weight & overage: its own clearly separated card --- */
.overage-panel {
  margin-top: 1.5rem; padding: 1.25rem 1.35rem;
  background: var(--panel); border: 1px solid var(--border);
  border-left: 3px solid var(--copper); border-radius: var(--radius);
}
.overage-panel .detail-sec-h { color: var(--copper-bright); }
.ov-status { margin: 0 0 0.4rem; color: var(--muted); }
.ov-status strong { color: var(--text); }
.ov-hint { font-size: 0.85rem; margin: 0 0 0.6rem; }
.ov-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.6rem; }
.admin .ov-row input[type="number"] { width: 7rem; min-height: 2.5rem; padding: 0.35rem 0.5rem; }
.admin .ov-row input[type="file"] { flex: 1 1 12rem; min-width: 0; color: var(--muted); }
.ov-breakdown { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 0.6rem 0.85rem; margin: 0 0 0.7rem; }
.ov-dl { display: grid; grid-template-columns: auto 1fr; gap: 0.25rem 1rem; margin: 0; }
.ov-dl dt { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.02em; font-size: 0.8rem; color: var(--muted); }
.ov-dl dd { margin: 0; color: var(--text); }
.ov-overmax { margin: 0.5rem 0 0; }
.ov-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
#ov-msg { margin-top: 0.6rem; }
#ov-receipt-link a { color: var(--copper-bright); }

/* Customer booking summary disclosure (tonnage/overage) */
.summary-note { margin: 0.75rem 0 0; font-size: 0.85rem; color: var(--muted); line-height: 1.5; }

/* --- Dashboard --- */
/* Loud safety-net banner: possible missed payments. Impossible to miss. */
.dash-alert {
  display: block; margin: 0 0 1.25rem; padding: 0.9rem 1.1rem;
  background: #5b1e12; border: 1px solid #E7C98A; border-left: 4px solid #E7C98A;
  border-radius: var(--radius); color: #FBE9D0; text-decoration: none;
  font-family: var(--font-display); font-weight: 600; letter-spacing: 0.02em;
}
.dash-alert:hover { background: #6d2417; }
.dash-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 0.75rem; margin: 0 0 1.5rem; }
.dash-card {
  display: flex; flex-direction: column; gap: 0.15rem;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0.9rem 1rem; text-decoration: none; color: var(--text);
}
.dash-card:hover { border-color: var(--steel); }
.dash-stat { font-family: var(--font-display); font-weight: 700; font-size: 1.9rem; line-height: 1; color: var(--price); }
.dash-stat-sub { font-size: 0.9rem; font-weight: 600; color: var(--muted); }
.dash-label { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.75rem; color: var(--steel); }
.dash-panels { display: grid; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); gap: 1.25rem; align-items: start; }
.dash-panel-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.dash-panel-head .admin-h { margin-top: 0.5rem; flex: 1; }
.dash-panel-link { font-size: 0.85rem; color: var(--steel); text-decoration: none; white-space: nowrap; }
.dash-panel-link:hover { color: var(--text); }
.dash-joblist-group { margin-bottom: 0.75rem; }
.dash-joblist-h { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.75rem; color: var(--muted); margin: 0 0 0.4rem; }
.dash-joblist-h .count { color: var(--steel); }
.dash-joblist { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.4rem; }
.dash-job {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.6rem;
  background: var(--panel); border: 1px solid var(--border); border-left: 3px solid var(--copper);
  border-radius: var(--radius); padding: 0.5rem 0.7rem;
}
.dash-job-date { font-family: var(--font-display); font-weight: 600; color: var(--copper-bright); font-size: 0.85rem; }
.dash-job-name { color: var(--text); font-weight: 600; }
.dash-job-meta { color: var(--muted); font-size: 0.85rem; }
.dash-updated { font-size: 0.85rem; margin-top: 1.5rem; }

/* --- Admin traffic (GA4) --- */
.admin .tf-range { background: transparent; color: var(--steel); border-color: var(--border-strong); }
.admin .tf-range.is-active { background: var(--copper); color: #14110E; border-color: var(--copper); }
.tf-note { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.25rem 1.4rem; max-width: 44rem; }
.tf-note-warn { border-left: 3px solid var(--copper); }
.tf-note .admin-h { margin-top: 0; border: 0; padding: 0; }
.tf-note code { background: var(--panel-2); padding: 0.1rem 0.35rem; border-radius: 4px; font-size: 0.9em; }
.tf-galink { margin-top: 1.25rem; }
.tf-galink a { color: var(--copper-bright); font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.8rem; }

/* KPI row */
.tf-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 0.75rem; margin-bottom: 1.5rem; }
.tf-kpi { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 0.9rem 1rem; display: flex; flex-direction: column; gap: 0.2rem; }
.tf-kpi-val { font-family: var(--font-display); font-weight: 700; font-size: 1.9rem; line-height: 1; color: var(--price); }
.tf-kpi-label { font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.72rem; color: var(--steel); }
.tf-kpi-change { font-size: 0.8rem; color: var(--muted); margin-top: 0.15rem; }
.tf-kpi-change.up { color: #9FD8B0; }
.tf-kpi-change.down { color: #E79A8A; }
.tf-kpi-vs { color: var(--muted); }

/* Panels + grid */
.tf-panel { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.1rem; margin-bottom: 1.25rem; }
.tf-panel .admin-h { margin-top: 0; }
.tf-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: 1.25rem; align-items: start; }
.tf-grid .tf-panel { margin-bottom: 0; }

/* SVG trend chart */
.tf-chart svg { width: 100%; height: auto; display: block; }
.tf-gridline { stroke: var(--border-strong); stroke-width: 1; }
.tf-line { fill: none; stroke: var(--copper); stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; }
.tf-area { fill: var(--copper); opacity: 0.14; stroke: none; }
.tf-dot { fill: var(--copper-bright); }
.tf-axis { fill: var(--muted); font-size: 16px; font-family: sans-serif; }
.tf-axis-x { text-anchor: middle; }

/* Ranked bar lists */
.tf-bar-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.4rem; }
.tf-bar-row { display: grid; grid-template-columns: minmax(6rem, 1.4fr) 2fr auto; align-items: center; gap: 0.6rem; }
.tf-bar-key { color: var(--text); font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tf-bar-track { height: 8px; background: var(--panel-2); border-radius: 999px; overflow: hidden; }
.tf-bar-share { display: block; height: 100%; background: var(--copper); border-radius: 999px; }
.tf-bar-val { font-family: var(--font-display); font-weight: 600; color: var(--price); font-size: 0.9rem; text-align: right; min-width: 2.5rem; }

/* Geography — extra weight */
.tf-geo { border-left: 3px solid var(--copper); }
.tf-city-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.5rem; }
.tf-city { display: grid; grid-template-columns: auto minmax(6rem, 1.3fr) 2fr auto; align-items: center; gap: 0.6rem; }
.tf-city-rank { font-family: var(--font-display); font-weight: 700; color: var(--steel); width: 1.2rem; text-align: right; }
.tf-city-name { color: var(--text); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tf-city-bar { height: 10px; background: var(--panel-2); border-radius: 999px; overflow: hidden; }
.tf-city-fill { display: block; height: 100%; background: linear-gradient(90deg, var(--copper), var(--copper-bright)); border-radius: 999px; }
.tf-city-val { font-family: var(--font-display); font-weight: 700; color: var(--price); text-align: right; min-width: 2.5rem; }

/* Devices split bar */
.tf-dev-bar { display: flex; height: 20px; border-radius: 999px; overflow: hidden; background: var(--panel-2); }
.tf-dev-seg { display: block; height: 100%; }
.tf-dev-desktop { background: var(--copper); }
.tf-dev-mobile { background: var(--copper-bright); }
.tf-dev-tablet { background: var(--steel); }
.tf-dev-legend { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; margin-top: 0.6rem; color: var(--muted); font-size: 0.85rem; }
.tf-dev-item { display: inline-flex; align-items: center; gap: 0.35rem; text-transform: capitalize; }
.tf-dev-dot { width: 0.7rem; height: 0.7rem; border-radius: 2px; display: inline-block; }

@media (max-width: 45.99rem) {
  .tf-kpi-val { font-size: 1.6rem; }
  .tf-bar-row { grid-template-columns: minmax(5rem, 1.2fr) 1.6fr auto; }
  .tf-city { grid-template-columns: auto minmax(5rem, 1.2fr) 1.6fr auto; }
}

/* Admin on a NARROW SCREEN: Adam works this one-handed in the field, and on an
   Android tablet in portrait. Columns already stack (auto-fit min 15rem); here
   we grow tap targets to >=44px and make the detail-panel actions full-width
   and easy to hit.

   THE BOUNDARY IS 45.99rem (735.84px), NOT 40rem. At 40rem this block stopped
   at 640px while the next rules up began at 44rem (704) and 46rem (736) — so
   641-703px matched nothing and fell through to the desktop layout. Adam's
   tablet sits in that gap. The cliff was measured on the dashboard heat-line:
   at 640 one full-width column per stage, at 660 four stages abreast at 134px
   each, and tap targets dropping 44px -> 40px across the same pixel.

   45.99 rather than 46 so this hands over to the min-width:46rem rules instead
   of overlapping them for one pixel — the same convention the line editor's
   34-45.99rem tablet regime already uses to meet its 46rem desktop regime.

   Five sibling blocks above share this boundary (cost rows, invoice rows, the
   forge plates, the heat line, traffic bars). They were moved together; a
   stylesheet where "narrow" means 640px in one block and 736px in the next is
   the bug this batch exists to remove. The .step-nav block near the top of the
   file deliberately stays at 40rem — it is shared with the public booking
   wizard, and this pass is scoped to the admin. */
@media (max-width: 45.99rem) {
  .admin button { min-height: 2.75rem; }
  /* Nav wraps to its own rows; links stay comfortably tappable even at 360px. */
  .admin-nav-link { min-height: 2.75rem; }
  .admin-nav-links { gap: 0.3rem; }
  .admin .inv-units,
  .admin .inv-save,
  .admin .lead-status,
  .admin .lead-save,
  /* .lead-convert belongs with the two above and was missed: each of them
     declares its own 2.5rem, which outranks the `.admin button` rule at the top
     of this block, so without being named here it stayed a 40px target on the
     phone while every other control in the same row got 44. */
  .admin .lead-convert,
  .pickup-inline input[type="date"] { min-height: 2.75rem; }
  .inv-edit { gap: 0.5rem 0.6rem; }
  .admin .inv-units { flex: 1 1 6rem; width: auto; }

  /* Tighter modal chrome so long values aren't jammed against the edges. */
  .detail-panel { padding: 1.15rem 1.15rem 1.35rem; }
  .detail-kv { grid-template-columns: 1fr; gap: 0.85rem; }

  .detail-actions { gap: 0.6rem; }
  .detail-actions > button,
  .detail-actions .pickup-inline { flex: 1 1 100%; width: 100%; }
  .detail-actions--tight > button { flex: 0 1 auto; width: auto; }
  .detail-actions .pickup-inline { display: flex; }
  .detail-actions .pickup-inline input[type="date"] { flex: 1 1 auto; width: auto; }
  .overage-panel { padding: 1.1rem; }
}

/* ===========================================================================
   About page — veteran-owned badge, owner's letter, service cards.
   Reuses existing tokens; copper is emphasis only.
   =========================================================================== */

/* Veteran-owned badge: a forged plate with a copper mark. Prominent, not clipart. */
.vet-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  margin-top: 1.1rem;
  padding: 0.5rem 0.95rem;
  background: var(--panel-2);
  border: 1px solid var(--copper);
  border-radius: 999px;
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.92rem;
  color: var(--text);
}
.vet-badge-mark { color: var(--copper-bright); font-size: 1.05rem; line-height: 1; }

/* Owner's tagline sitting under the "The Forge" H1 — forged engraved subhead. */
.page-tagline {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  font-size: clamp(1.1rem, 2.6vw, 1.5rem);
  line-height: 1.1;
  color: var(--steel);
  margin: 0.1rem 0 0.25rem;
}

/* The About section gets its OWN reading column — wider than the 40rem guides
   narrow so that, after the floated photo, the wrapped prose still holds a
   comfortable ~65-75 char measure. Head, letter, pull-quote and sign-off all
   share this one width for a single consistent column. */
.container-letter { max-width: 57rem; }

/* Crew photo woven INTO the letter (not a banner). Desktop: floats into the
   reading column so the prose wraps around it — no caption, hairline forged
   edge. Mobile: collapses to a full-width image at the same paragraph break.
   aspect-ratio reserves the box per breakpoint → no layout shift. */
.letter-photo {
  float: right;
  width: 20rem;
  margin: 0.4rem 0 1rem 1.75rem; /* flush to the column's right edge; gutter on the text side */
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.letter-photo img { display: block; width: 100%; height: auto; aspect-ratio: 4 / 5; object-fit: cover; }
/* Keep the pull-quote and sign-off from riding up beside the float. */
.prose .pull-quote { clear: right; }
.letter-signoff { clear: both; }

@media (max-width: 43.99rem) {
  .letter-photo { float: none; width: auto; margin: 1.6rem 0; }
  .letter-photo img { aspect-ratio: 3 / 2; }
}

/* Pull-quote inside the owner's letter. */
.pull-quote {
  margin: 1.85rem 0;
  padding: 0.25rem 0 0.25rem 1.15rem;
  border-left: 3px solid var(--copper);
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  font-size: clamp(1.25rem, 3.6vw, 1.65rem);
  line-height: 1.18;
  color: var(--text);
}

/* Signature sign-off line. */
.letter-signoff { margin-top: 2rem; padding-top: 1.25rem; border-top: 1px solid var(--border); }
.letter-signoff .signed { font-family: var(--font-wordmark); font-size: 1.25rem; color: var(--steel); letter-spacing: 0.02em; }
.letter-signoff .signed-role { display: block; color: var(--muted); font-size: 0.9rem; margin-top: 0.35rem; }

/* "What we do" service cards — data-driven from services.js. */
.about-svc-grid { list-style: none; margin: 1.5rem 0 0; padding: 0; display: grid; grid-template-columns: 1fr; gap: 1rem; }
@media (min-width: 34rem) { .about-svc-grid { grid-template-columns: repeat(2, 1fr); } }
.about-svc-card { display: flex; flex-direction: column; gap: 0.5rem; height: 100%; padding: 1.25rem; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); }
.about-svc-card:hover { border-color: var(--copper); color: var(--text); }
.about-svc-name { font-family: var(--font-display); font-weight: 700; text-transform: uppercase; letter-spacing: 0.01em; font-size: 1.2rem; }
.about-svc-blurb { color: var(--muted); font-size: 0.92rem; line-height: 1.55; }
.about-svc-cta { margin-top: auto; font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.8rem; color: var(--copper); }
.about-area-line { margin-top: 1.5rem; }

/* --- Testimonials / Google reviews (plain on-page, no rating schema) --- */
.reviews-rating {
  display: flex; align-items: center; gap: 0.5rem;
  margin: 0.4rem 0 0;
  font-family: var(--font-display); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em;
  font-size: 0.95rem; color: var(--steel);
}
.review-stars { color: var(--copper-bright); letter-spacing: 0.1em; line-height: 1; }
/* align-items:start so cards size to their own content — long reviews just run
   taller, no clamp, full verbatim text. */
.reviews-grid {
  list-style: none; margin: 1.5rem 0 0; padding: 0;
  display: grid; grid-template-columns: 1fr; gap: 1rem; align-items: start;
}
@media (min-width: 40rem) { .reviews-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .reviews-grid { grid-template-columns: repeat(3, 1fr); } }
.review-card {
  display: flex; flex-direction: column; gap: 0.75rem;
  padding: 1.35rem;
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
}
.review-card .review-stars { font-size: 1.05rem; }
.review-text { margin: 0; color: var(--text); font-size: 0.96rem; line-height: 1.6; }
.review-meta { margin-top: 0.15rem; display: flex; flex-direction: column; gap: 0.1rem; padding-top: 0.5rem; border-top: 1px solid var(--border); }
.review-name { font-family: var(--font-display); font-weight: 700; text-transform: uppercase; letter-spacing: 0.02em; font-size: 1rem; color: var(--text); }
.review-service { color: var(--muted); font-size: 0.85rem; }
.review-via { color: var(--muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; margin-top: 0.1rem; }
/* "Leave a Google Review" — centered, secondary action under the review grid. */
.reviews-cta { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0.75rem; margin: 1.5rem 0 0; text-align: center; }
.reviews-cta-lead { color: var(--muted); font-size: 0.95rem; }

/* Footer veteran-owned line — understated, on every page. */
.footer-vet { display: flex; align-items: center; gap: 0.4rem; margin: 0.15rem 0 0.6rem; font-size: 0.8rem; letter-spacing: 0.04em; color: var(--muted); }
.footer-vet .vet-mark { color: var(--copper); }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
}

/* --- Change-order signing page + admin CO controls (Operations 8b) ---
   The public page reuses the contract signing page's classes wholesale
   (.sign-terms, .sign-lines, .sign-form, .sign-agree) — it IS the same document
   flow, and a client who has signed a contract here should find the change
   order behaves identically. These are the two additions it needs. */
.sign-parent { margin: -0.35rem 0 1.25rem; font-size: 0.92rem; }
/* Spelled-out arithmetic under the price adjustment, so nobody reads the
   adjustment as the project's new total. */
.sign-newtotal { margin: 0.4rem 0 0; font-size: 0.9rem; }

/* Admin: per-change-order state lines, sharing the contract-level classes. */
.co-signed { display: flex; flex-direction: column; align-items: flex-start; gap: 0.4rem; margin-top: 0.5rem; }
.co-item .ct-awaiting, .co-item .ct-signed { margin: 0.5rem 0 0; }

/* --- Booking closeout sequence (pickup → weigh → final → complete) ---
   A numbered ladder rather than a row of buttons: the point of this panel is
   that the steps are ORDERED, and a flat action bar can't say that. Done steps
   recede, the current one is picked out in copper, future ones stay quiet. */
.closeout-steps { list-style: none; padding: 0; margin: 0 0 0.9rem; display: grid; gap: 0.5rem; counter-reset: none; }
.closeout-step { display: grid; grid-template-columns: 1.6rem 1fr; gap: 0.6rem; align-items: start; opacity: 0.55; }
.closeout-step.is-done, .closeout-step.is-current { opacity: 1; }
.cs-n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.6rem; height: 1.6rem; border-radius: 50%;
  font-family: var(--font-display); font-weight: 700; font-size: 0.8rem;
  background: var(--panel-2); color: var(--muted); border: 1px solid var(--border-strong);
}
.closeout-step.is-done .cs-n { background: var(--border-strong); color: var(--text); }
.closeout-step.is-current .cs-n { background: var(--copper); color: #14110E; border-color: var(--copper); }
.cs-body { display: grid; gap: 0.1rem; font-size: 0.9rem; }
.cs-body strong { color: var(--text); }
.cs-body .muted { font-size: 0.83rem; }
.closeout-step.is-current .cs-body strong { color: var(--copper-bright); }
.admin .closeout .detail-actions button[disabled] { opacity: 0.45; cursor: not-allowed; }

/* --- Booking pause (admin toggle + public out-of-stock) ---
   The admin side is deliberately loud: a paused shop takes no money, and the
   failure mode is forgetting it's on. The public side is deliberately quiet —
   being closed is a normal business state, not an error. */
.dash-paused {
  display: block; padding: 0.9rem 1rem; margin: 0 0 1rem;
  background: rgba(196, 99, 44, 0.14); border: 1px solid var(--copper);
  border-left: 4px solid var(--copper); border-radius: var(--radius);
  color: var(--text); text-decoration: none;
}
.dash-paused strong { display: block; color: var(--copper-bright); font-family: var(--font-display); letter-spacing: 0.03em; }
.dash-paused span { font-size: 0.9rem; color: var(--muted); }
.dash-paused:hover { background: rgba(196, 99, 44, 0.2); color: var(--text); }

.set-pause { display: grid; gap: 0.6rem; padding: 0.9rem 1rem; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); }
.set-pause.is-paused { border-color: var(--copper); border-left: 4px solid var(--copper); background: rgba(196, 99, 44, 0.08); }
.set-pause-state { margin: 0; font-size: 0.92rem; color: var(--muted); }
.set-pause.is-paused .set-pause-state strong { color: var(--copper-bright); }
.set-pause-state strong { color: var(--text); }
.admin .set-pause textarea { min-height: 3.5rem; font-size: 1rem; }
.set-pause-note { margin: 0; font-size: 0.83rem; }

/* Public: the out-of-stock panel that replaces the wizard. */
.paused-panel { max-width: 34rem; margin: 0 auto; text-align: center; display: grid; gap: 0.75rem; }
.paused-lead { margin: 0; font-size: 1.15rem; color: var(--text); }
.paused-panel .hero-actions { justify-content: center; }

/* --- Junk removal: the two offers (bin / bin + helpers) ---
   Peers by design: same .info-card furniture, same column width, so neither
   reads as the lesser option. They're distinguished by their ACTIONS, not by
   weight — one books itself, the other opens a conversation — which is the
   honest distinction while helpers have no pricing behind them. */
.offer-grid { grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); }
.offer-card { display: flex; flex-direction: column; }
/* Pushes the buttons to the card's foot so both CTAs line up across cards of
   unequal text length. */
.offer-card .hero-actions { margin-top: auto; padding-top: 0.35rem; }
.offer-note { font-size: 0.88rem; margin-bottom: 0.75rem; }

/* --- Special delivery notes (booking) ---
   A callout, not a plain field. Adam reads the booking detail on a phone
   before he drives; a gate code or a "left side of the driveway" that reads
   like every other row is one he skims past, and that's a wasted trip. */
.detail-notes {
  padding: 0.6rem 0.75rem;
  background: rgba(196, 99, 44, 0.1);
  border: 1px solid var(--copper);
  border-left: 3px solid var(--copper);
  border-radius: var(--radius);
}
.detail-notes .detail-field-label { color: var(--copper-bright); }
/* Customer text — preserve the line breaks they typed. */
.detail-notes-val { white-space: pre-wrap; }

/* Estimate line: the In-House $ cost column (estimate builder only — the
   contract editor never renders it) and the name field. */
.el-f-name input, .el-f-inhouse input { width: 100%; }
.est-line--costed { --el-cost: 1; }

/* The "#" reorder field: type a new number, the line moves there. Narrow at
   every width — on a phone the row stacks, and a full-width box for a
   two-character number would read as a major field, which it isn't. */
.el-f-pos { max-width: 4.5rem; }
.el-f-pos input { width: 100%; text-align: center; font-variant-numeric: tabular-nums; }

/* The heart box — Adam's margin. Admin-only, collapsed by default. */
.est-profit-wrap { margin: 0.4rem 0 1rem; }
.est-heart {
  background: none; border: 0; cursor: pointer; padding: 0.2rem 0.3rem;
  font-size: 1.15rem; line-height: 1; opacity: 0.75;
}
.est-heart:hover, .est-heart[aria-expanded="true"] { opacity: 1; }
.est-profit {
  margin-top: 0.5rem; padding: 0.85rem 1rem;
  background: var(--panel); border: 1px solid var(--border);
  border-left: 3px solid var(--copper); border-radius: var(--radius);
}
.est-profit-grid {
  display: grid; grid-template-columns: 1fr auto; gap: 0.3rem 1rem; margin: 0;
}
.est-profit-grid dt { color: var(--muted); }
.est-profit-grid dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }
.est-profit-grid .ep-profit { font-weight: 700; }
.est-profit-note { margin: 0.7rem 0 0; color: var(--muted); font-size: 0.86rem; line-height: 1.5; }

/* Signing pages: the line's bold client-facing name above its description. */
.sign-line-name { display: inline-block; }

/* Estimate: client-facing adjustments (0031) — county tax + project discount. */
.est-adjust {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 1.1rem;
  margin: 0.6rem 0 0.4rem; padding: 0.6rem 0.8rem;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
  font-size: 0.92rem;
}
.est-tax-toggle { display: inline-flex; align-items: center; gap: 0.4rem; }
.est-tax-pct input { width: 4.5rem; }
/* 0103 — the deposit percentage, in the row that already holds the other
   per-estimate terms. It sits between county tax and the project discount
   because that is the order the estimate reads in: what is charged, what is
   owed up front, what comes off. Same shape as its neighbours, no new
   vocabulary. */
.est-deposit { display: inline-flex; align-items: center; gap: 0.4rem; }
.est-deposit input { width: 4.5rem; }
.est-proj-disc { display: inline-flex; align-items: center; gap: 0.4rem; }
.est-proj-disc select { width: 3.6rem; }
.est-proj-disc input { width: 6rem; }
.est-breakdown { display: grid; gap: 0.15rem; margin: 0.4rem 0 0.2rem; }
.et-row { display: flex; justify-content: space-between; color: var(--muted); font-size: 0.92rem; }
.et-row span:last-child { font-variant-numeric: tabular-nums; }
/* Native number spinners are hidden across the estimate builder's line rows and
   the project-adjustment strip: on a narrow phone the up/down arrows crowd — and
   in the discount control, visually collide with — the %/$ symbol beside them.
   inputmode="decimal" already gives phones the right keypad, so the arrows add
   nothing here. */
.est-line input[type="number"]::-webkit-outer-spin-button,
.est-line input[type="number"]::-webkit-inner-spin-button,
.est-adjust input[type="number"]::-webkit-outer-spin-button,
.est-adjust input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.est-line input[type="number"],
.est-adjust input[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
}

/* The per-line %/$ discount control. Comfortable widths so the %/$ toggle always
   shows its symbol (plus the select's own chevron) and the value field has room;
   the value flexes to fill a wide row but never gets cramped on a 375px phone. */
.el-f-disc .el-disc-wrap { display: flex; gap: 0.4rem; align-items: center; }
.el-disc-type { flex: 0 0 auto; width: 4.25rem; }
.el-disc-val { flex: 1 1 6rem; min-width: 5rem; max-width: 9rem; }

/* Per-line "non-taxable" (0047). Every other .el-f is a stacked label-over-input
   column; a checkbox reads as box-then-words, so this one overrides to a row.
   The whole label is the hit area, sized past the 44px touch minimum by the row
   height rather than by padding. The box itself has to opt OUT of the
   `.admin .est-line input { min-height: 2.75rem }` rule, which would otherwise
   stretch a checkbox into a lozenge — same override .inv-check already makes. */
.est-line .el-f-notax {
  flex-direction: row; align-items: center; gap: 0.45rem;
  min-height: 2.75rem; cursor: pointer; color: var(--muted);
  font-size: 0.92rem; white-space: nowrap;
}
.admin .est-line input.el-notax {
  width: 1.15rem; height: 1.15rem; min-height: 0;
  flex: none; margin: 0; accent-color: var(--copper); cursor: pointer;
}

/* Signing pages: the adjustment rows above the total. */
.sign-subline {
  display: flex; justify-content: space-between;
  margin: 0.15rem 0; color: var(--muted); font-size: 0.94rem;
}

/* Visually hidden but read by screen readers — the library picker's label. */
.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;
}

/* Line Items library (0033) — the gear page. */
.li-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.li-lead { max-width: 46rem; margin-top: 0.2rem; }
.li-form { display: grid; grid-template-columns: 1fr 1.5fr 7rem 7rem; gap: 0.6rem; align-items: end; }
.li-add { margin: 0.8rem 0 1.4rem; padding: 0.9rem 1rem; background: var(--panel);
  border: 1px solid var(--border); border-left: 3px solid var(--copper); border-radius: var(--radius); }
.li-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.li-item { display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.7rem 0.9rem; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); }
.li-item.is-editing { display: block; }
.li-read { display: flex; align-items: baseline; gap: 0.8rem; flex-wrap: wrap; min-width: 0; }
.li-name { font-size: 1rem; }
.li-desc { font-size: 0.9rem; }
.li-money { display: inline-flex; gap: 0.9rem; font-variant-numeric: tabular-nums; }
.li-k { color: var(--muted); font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.04em; }
.li-actions { display: flex; gap: 0.5rem; align-items: center; flex-shrink: 0; margin-top: 0.5rem; }
.li-item .li-actions { margin-top: 0; }

/* --- Materials (WO-0079 d): the catalogue at store prices -------------------
   The Line Items page's frame (.li-*) with what a catalogue row needs on top:
   an eight-field form with pack questions that appear and disappear, a
   retired row greyed in its WORDS only (its Restore button is a full control,
   never a greyed one), and the answers the gateway gives about bundles. The
   ci- prefix is the engine's word, catalogue: no identifier says "materials"
   (docs/wo79-pricing-engine.md §1). Its phone rules are at the end of this
   block, not in the shared 700px block below. */
.li-form.ci-form { grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: end; }
.ci-f-name, .ci-f-notes { grid-column: span 2; }
.ci-f select { width: 100%; }
.ci-check { display: flex; align-items: center; gap: 0.6rem; min-height: 2.75rem; cursor: pointer; }
.ci-check input { width: 1.25rem; height: 1.25rem; margin: 0; flex-shrink: 0; }
.ci-f-size[hidden], .ci-f-pname[hidden] { display: none; }
.ci-hint { display: block; font-size: 0.8rem; margin-top: 0.2rem; }
.ci-listhead { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; }
.ci-read { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.ci-title { display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; }
.ci-facts { display: flex; gap: 0.9rem; flex-wrap: wrap; font-size: 0.92rem; }
.ci-price { font-variant-numeric: tabular-nums; }
.ci-notes, .ci-said, .ci-used { overflow-wrap: anywhere; }
.ci-said, .ci-used { font-size: 0.88rem; color: var(--text); }
.ci-used { margin: 0.6rem 0 0; max-width: 46rem; }
.ci-chip { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted);
  border: 1px solid var(--border-strong); border-radius: var(--radius); padding: 0.05rem 0.4rem; }
.ci-item.is-retired { border-style: dashed; }
.ci-item.is-retired .ci-read { opacity: 0.6; }
.admin .ci-item .btn, .admin .ci-add .btn, .admin .ci-toggle { min-height: 2.75rem; }
.ci-msg { flex-basis: 100%; }
@media (max-width: 700px) {
  /* .li-form.ci-form: the shared 700px block below sets .li-form to two
     columns and comes later at the same specificity, so a bare .ci-form lost. */
  .li-form.ci-form { grid-template-columns: minmax(0, 1fr); }
  .ci-f-name, .ci-f-notes { grid-column: auto; }
  .ci-item .li-actions { flex-wrap: wrap; }
}

/* --- Bundles and the bundle wizard (WO-0079 d) --------------------------------
   The list is the Line Items / Materials frame (.li-*); the wizard is a
   .modal-panel--wide with WO-0018's step strip, its own copy (bw-steps), so a
   change to the change order's styling can't move this one. Existing tokens
   only. Its phone rules are at the end of this block. */
.bw-listhead { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; margin: 0.8rem 0; }
.bw-filters { display: inline-flex; gap: 0.4rem; flex-wrap: wrap; }
.bw-filter[aria-pressed="true"] { border-color: var(--copper); color: var(--text); }
.bw-read { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.bw-name-line { display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; }
.bw-chip { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; padding: 0.05rem 0.4rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius); color: var(--muted); }
.bw-chip--live { border-color: var(--copper); color: var(--copper); }
.bw-row-msg { flex-basis: 100%; }
.admin .bw-item .btn, .admin .bw-listhead .btn { min-height: 2.75rem; }

.bw-steps {
  list-style: none; counter-reset: bw-step; display: flex; flex-wrap: wrap; gap: 0.35rem 1rem;
  margin: 0 0 0.9rem; padding: 0; font-size: 0.85rem; color: var(--muted);
}
.bw-steps li { counter-increment: bw-step; }
.bw-steps li::before { content: counter(bw-step) ". "; }
.bw-steps li.is-current { color: var(--text); font-weight: 700; }
.bw-steps li.is-done { color: var(--steel); }
.bw-lead { margin: 0 0 0.75rem; font-size: 0.9rem; }
.bw-check { display: flex; align-items: center; gap: 0.6rem; min-height: 2.75rem; cursor: pointer; }
.bw-check input { width: 1.25rem; height: 1.25rem; margin: 0; flex-shrink: 0; }
.bw-measure-list, .bw-items { list-style: none; margin: 0 0 0.75rem; padding: 0; display: grid; gap: 0.6rem; }
.bw-measure { padding: 0.75rem 0.85rem; }
.bw-measure-grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 0.6rem; align-items: end; }
.bw-hint { margin: 0.4rem 0 0; font-size: 0.85rem; }
.bw-remove { margin-top: 0.5rem; }
.bw-recipe-m { padding: 0.75rem 0.85rem; margin: 0 0 0.75rem; }
.bw-recipe-h { margin: 0 0 0.5rem; font-size: 0.95rem; }
.bw-item-row { display: grid; grid-template-columns: minmax(0, 1fr) 8rem auto; gap: 0.6rem; align-items: end; }
.bw-item-row .bw-remove { margin-top: 0; }
.bw-item-row select { width: 100%; }
.bw-qty-msg { grid-column: 1 / -1; margin: -0.3rem 0 0; font-size: 0.85rem; }
.bw-item-row.is-incomplete input[data-bw-if="qty"] { border-color: var(--danger, #e06c5a); }
.bw-repeat { margin: 0 0 0.6rem; padding: 0.5rem 0.65rem; border: 1px solid var(--border); border-left: 3px solid var(--copper); border-radius: 6px; }
.bw-repeat p { margin: 0 0 0.4rem; font-size: 0.88rem; }
.bw-uncounted { margin: 0.4rem 0 0; padding-left: 1.1rem; font-size: 0.88rem; color: var(--danger, #e06c5a); }
.bw-recipe-tools { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.bw-newcat { margin-top: 0.75rem; padding: 0.75rem 0.85rem; }
.bw-newcat-h { margin: 0 0 0.5rem; font-weight: 700; font-size: 0.9rem; }
.bw-newcat-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.6rem; align-items: end; }
.bw-newcat-grid [hidden] { display: none; }
.bw-example, .bw-try { margin-top: 0.75rem; padding: 0.75rem 0.85rem; overflow-wrap: anywhere; }
.bw-example-h { margin: 0 0 0.4rem; font-weight: 700; font-size: 0.9rem; }
.bw-answer { display: flex; align-items: end; gap: 0.6rem; flex-wrap: wrap; margin: 0 0 0.6rem; }
.bw-answer-name { font-weight: 700; }
.bw-mode { display: inline-flex; gap: 0.4rem; }
.bw-mode [aria-pressed="true"] { border-color: var(--copper); color: var(--text); }
.bw-line { margin: 0 0 0.6rem; }
.bw-line-name { margin: 0; font-weight: 700; }
.bw-line-desc, .bw-line-tax { margin: 0.15rem 0 0; font-size: 0.88rem; }
.bw-breakdown { list-style: none; margin: 0 0 0.6rem; padding: 0; display: grid; gap: 0.2rem; font-size: 0.9rem; }
.bw-breakdown li { display: flex; justify-content: space-between; gap: 0.75rem; }
.bw-breakdown .bw-total { font-weight: 700; border-top: 1px solid var(--border); padding-top: 0.25rem; }
.bw-buy { width: 100%; border-collapse: collapse; font-size: 0.88rem; margin: 0 0 0.4rem; }
.bw-buy th, .bw-buy td { text-align: left; padding: 0.25rem 0.4rem 0.25rem 0; border-bottom: 1px solid var(--border); }
.bw-buy .num, .bw-breakdown .num, .bw-money .num { text-align: right; font-variant-numeric: tabular-nums; }
.bw-buy-words { overflow-wrap: anywhere; }
.bw-taxline { margin: 0 0 0.6rem; font-size: 0.85rem; }
.bw-money { display: grid; grid-template-columns: auto auto; justify-content: start; gap: 0.15rem 1.25rem; margin: 0 0 0.5rem; }
.bw-money dd { margin: 0; }
.bw-money .is-loss { color: var(--danger, #e06c5a); }
/* WO-0102 — the bundle's minimum charge: its help on step 1, "Minimum applies" on a price, the summary line. */
.bw-help { margin: -0.2rem 0 0.6rem; font-size: 0.85rem; }
.bw-minimum-applies { margin: 0.35rem 0 0.5rem; font-weight: 700; font-size: 0.9rem; }
.bw-final-minimum { margin: 0.25rem 0; }
.bw-warnings { margin: 0.4rem 0 0; padding-left: 1.1rem; font-size: 0.88rem; }
.bw-final-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 0.6rem 0; }
.admin .bw-panel .btn { min-height: 2.75rem; }
.bw-ask { margin: 0.6rem 0; }
@media (max-width: 700px) {
  .bw-measure-grid, .bw-newcat-grid { grid-template-columns: minmax(0, 1fr); }
  .bw-item-row { grid-template-columns: minmax(0, 1fr) 6.5rem; }
  .bw-item-row .bw-remove { grid-column: 1 / -1; justify-self: start; }
  .bw-buy { font-size: 0.82rem; }
}

/* Estimate builder: the gear beside "Add line" (opens the Line Items library). */
.est-line-tools { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.est-lib-gear { display: inline-flex; align-items: center; color: var(--muted); padding: 0.2rem; }
.est-lib-gear:hover { color: var(--copper); }
.el-tolib { white-space: nowrap; }

/* Line-photo thumbnail as a tap target — an unpadded button wrapping the image
   so tapping opens the lightbox instead of the label's file picker. */
.el-img-view { padding: 0; border: 0; background: none; cursor: zoom-in; line-height: 0; border-radius: var(--radius); }
.el-img-view:focus-visible { outline: 2px solid var(--copper); outline-offset: 2px; }

/* Item-name type-ahead: a filtered list of saved items under the Name input.
   Absolutely positioned so it overlays following fields rather than shoving the
   row down; the wrapper is the positioning context. */
.el-name-ac { position: relative; display: block; }
.el-name-suggest {
  position: absolute; top: calc(100% + 2px); left: 0; right: 0; z-index: 30;
  max-height: 15rem; overflow-y: auto;
  background: var(--panel-2, var(--panel)); border: 1px solid var(--border-strong);
  border-radius: var(--radius); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  padding: 0.25rem;
}
.el-name-suggest-item {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; width: 100%;
  padding: 0.6rem 0.6rem; min-height: 2.75rem; text-align: left;
  background: none; border: 0; border-radius: calc(var(--radius) - 2px); cursor: pointer;
  color: var(--text); font-family: var(--font-body); font-size: 0.95rem;
}
.el-name-suggest-item:hover, .el-name-suggest-item:focus-visible { background: var(--panel); outline: none; }
.el-ns-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.el-ns-price { color: var(--muted); font-variant-numeric: tabular-nums; flex-shrink: 0; }

/* Send confirmation (estimates + contracts): the inline confirm bar and the
   persisted "Sent to … — <when>" receipt. Not a modal — it sits right under the
   Send button. */
/* `hidden` must actually hide it. The rule below sets display:flex, and an
   author class beats the user-agent's [hidden]{display:none} — so the confirm
   host sat in every row at 24px tall plus a gap, an empty box waiting for a
   question nobody had asked yet. Measured before fixing: hidden=true,
   display=flex, height=24. */
.send-confirm[hidden] { display: none; }
.send-confirm {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.9rem;
  margin: 0.6rem 0; padding: 0.7rem 0.9rem;
  background: var(--panel); border: 1px solid var(--border-strong); border-left: 3px solid var(--copper);
  border-radius: var(--radius);
}
.send-confirm .sc-q { color: var(--text); }
.send-confirm .sc-actions { display: inline-flex; gap: 0.5rem; }
/* 0096 — the confirm's optional second question ("tell the client"). Its own
   line on a narrow screen and inline beside the question on a wide one, which
   is what the host's flex-wrap already does; this only styles the label. */
.send-confirm .sc-opt {
  display: inline-flex; align-items: center; gap: 0.4rem;
  color: var(--text); font-size: 0.9rem;
}
.send-confirm .sc-opt input { margin: 0; }
.admin .send-confirm .btn { min-height: 2.5rem; font-size: 0.9rem; padding: 0.4rem 0.9rem; }
.send-receipt { margin: 0.45rem 0 0; font-size: 0.85rem; }
/* The exact timestamp beside the relative one. "Sent 2h ago" is what's read;
   "(11/08/2026, 20:04:23)" is what's needed when someone asks exactly when —
   dimmer and smaller, present without competing. */
.send-receipt-exact { color: var(--muted); opacity: 0.75; font-size: 0.92em; }

@media (max-width: 700px) {
  .li-form { grid-template-columns: 1fr 1fr; }
  .li-item { flex-direction: column; align-items: stretch; }
}

/* --- Financials: office bills (0078) --------------------------------------
   THE WORKSTATION THEME'S FIRST FULL NEW-PAGE CONSUMER. Everything structural
   on this page — the frame, the boxes, the bars, the chips — is the theme's and
   is defined with it further up. What lives here is only what a BILL LIST needs
   and nothing else does: the row, its facts line, and the two things that have
   to be legible at a glance on a page about money you owe.

   The theme's four constraints hold throughout: squared corners, existing
   tokens only, no decoration that could remove a control, and a [hidden] guard
   on every rule that declares `display`. */

/* The page's top rail. Same shape and the same hairline as the job page's
   .jd-rail--top; NOT the same class, because that one is scoped to the job
   dashboard and borrowing a page-local name across pages is how two screens end
   up sharing a rule neither of them can change. Two consumers is when a thing
   is promoted into the theme — see the same argument in admin-financials.js
   about the collapsible box. */
/* 0087 — PROMOTED FROM .ws-rail TO THE THEME. This was the Financials page's
   top rail; the Vendors page now wants the identical thing, and two consumers
   is the bar 0078's own header set for moving something into the theme rather
   than copying it a second time. One name, one rule, both pages.

   A page's rail: what the PAGE can do, on one line, above what it contains. */
.ws-rail {
  display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;
  margin: 0 0 0.9rem; padding-bottom: 0.9rem;
  border-bottom: 1px solid var(--border-strong);
}
.ws-rail[hidden] { display: none; }
.admin .ws-act { min-height: 2.75rem; font-size: 0.82rem; padding: 0.45rem 0.85rem; }

/* The Vendors page's search, on its own row under the rail — see the note in
   admin-forging-suppliers.js for why it stopped sharing one with the button. */
.sup-search-label { display: flex; flex-direction: column; gap: 0.25rem; margin: 0 0 0.85rem; max-width: 26rem; }

/* The back link at the top of a page that is reached from another page. Same
   vocabulary as .forging-nav-back, one level down. */
.admin-back {
  display: inline-block; margin: 0 0 0.35rem;
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 0.8rem; color: var(--muted); text-decoration: none;
}
.admin-back:hover { color: var(--copper-bright); }
.fin-msg { display: block; }
/* 0094 — the Resend gathering. Rows are LABELS wrapping their checkbox, so the
   whole row is the target; on a phone that is the difference between a control
   and a dare. */
.rs-list { max-height: 60vh; overflow-y: auto; margin-bottom: 0.75rem; }
.rs-group + .rs-group { margin-top: 0.9rem; }
.rs-group-h {
  margin: 0 0 0.3rem;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}
.rs-row {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
}
.rs-row + .rs-row { margin-top: 0.3rem; }
.rs-row:hover { border-color: var(--border-strong); }
.rs-check { flex: 0 0 auto; }
.rs-what { flex: 1 1 auto; min-width: 0; display: flex; flex-wrap: wrap; gap: 0.15rem 0.5rem; align-items: baseline; }
.rs-label { overflow-wrap: anywhere; }
.rs-detail { font-size: 0.85rem; font-variant-numeric: tabular-nums; }
.rs-state { flex: 0 0 auto; font-size: 0.8rem; color: var(--muted); white-space: nowrap; }
/* NEVER SENT reads differently because it IS different: sending a thing the
   first time is doing the job, re-sending is chasing. Adam asked for the two to
   be tellable apart at a glance. */
.rs-row--fresh { border-color: var(--copper); }
.rs-state.is-fresh { color: var(--copper-bright); }

@media (max-width: 560px) {
  .rs-row { flex-wrap: wrap; }
  .rs-state { flex-basis: 100%; padding-left: 1.6rem; }
}

/* 0092 — the three sections. A section is a HEADING plus its boxes, not a box
   itself: nesting boxes inside boxes would give every figure two bars to open
   before it could be read. */
.fin-section { margin: 0 0 1.75rem; }
.fin-section:last-child { margin-bottom: 0; }
.fin-section-title {
  margin: 0 0 0.15rem;
  font-family: var(--font-display);
  font-size: 1.05rem;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text);
}
.fin-section-note { margin: 0 0 0.75rem; font-size: 0.85rem; }
/* The definition, in the smallest voice, directly under the title it defines —
   a number that doesn't say what it is gets misquoted. */
.fin-basis { margin: 0 0 0.9rem; font-size: 0.85rem; font-style: italic; }

/* The profit stack. A TABLE because it is one: two periods, one column each,
   and the subtractions have to line up or they cannot be read as a stack. */
.fin-pnl { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.fin-pnl th, .fin-pnl td { padding: 0.35rem 0.5rem; text-align: right; }
.fin-pnl th[scope="row"] { text-align: left; font-weight: 400; color: var(--muted); }
.fin-pnl thead th { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
.fin-pnl tbody tr + tr td, .fin-pnl tbody tr + tr th { border-top: 1px solid var(--border); }
.fin-pnl-note { display: block; font-size: 0.75rem; }
.fin-pnl-muted th[scope="row"], .fin-pnl-muted td { opacity: 0.75; font-size: 0.9rem; }
.fin-pnl-total th[scope="row"] { color: var(--text); font-weight: 600; }
.fin-pnl-total td { font-weight: 600; border-top-width: 2px !important; }
.fin-pnl-pct th[scope="row"], .fin-pnl-pct td { font-size: 0.85rem; color: var(--muted); }
.fin-firstrun { margin: 0.75rem 0 0; font-size: 0.85rem; }

/* The two closed-job cards, side by side and equal — a big job should not beat
   a well-run one on layout either. */
.fin-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-top: 1rem; }
.fin-card { border: 1px solid var(--border); border-radius: var(--radius); padding: 0.7rem 0.8rem; }
.fin-card--empty { border-style: dashed; }
.fin-card-when { margin: 0 0 0.2rem; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; }
.fin-card-name { margin: 0 0 0.3rem; font-weight: 600; }
.fin-card-empty { margin: 0; font-size: 0.85rem; }
.fin-card-figures { margin: 0; display: flex; align-items: baseline; gap: 0.6rem; }
.fin-card-money { font-size: 1.15rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.fin-card-pct { color: var(--muted); font-variant-numeric: tabular-nums; }
.fin-cards-note { margin: 0.4rem 0 0; font-size: 0.8rem; }

.fin-asides { display: flex; flex-wrap: wrap; gap: 1.25rem; margin-top: 1rem; }
.fin-aside { margin: 0; display: flex; flex-direction: column; }
.fin-aside-k { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
.fin-aside-v { font-size: 1.1rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.fin-aside-note { font-size: 0.75rem; }

/* A job's line in the Projects section. */
.fin-jobs { list-style: none; margin: 0; padding: 0; }
.fin-job {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.9rem;
  padding: 0.5rem 0; border-bottom: 1px solid var(--border);
}
.fin-job:last-child { border-bottom: 0; }
.fin-job-name { flex: 1 1 12rem; font-weight: 600; }
.fin-job-fig { font-size: 0.85rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
.fin-job-k { color: var(--muted); }
.fin-job-profit { font-weight: 600; }
.fin-job-pct { color: var(--muted); font-weight: 400; }
/* Owed to a sub is a liability, not a cost already borne — the same copper the
   rest of the admin uses for money still outstanding. Rendered only when the
   figure is non-zero, so it reads as a flag rather than as a column. */
.fin-job-owed { color: var(--copper-bright); }

/* WO-0039b — CORRECTING A JOB EXPENSE, in the three places one shows. Each row
   that offers Edit grows a column for it; the 44px tap target stays. */
.jd-subrow.has-act { grid-template-columns: auto minmax(0, 1fr) auto auto; }
.jd-sub-act .jd-act { width: auto; min-width: 3.5rem; }
.cost-row.has-edit { grid-template-columns: auto 1fr auto auto 2.75rem; }
.cost-edit { min-height: var(--ws-tap); padding: 0 0.6rem; }
@media (max-width: 45.99rem) {
  .jd-subrow.has-act { grid-template-columns: minmax(0, 1fr) auto auto; }
  .admin .jd-sub-act .jd-act { width: auto; }
  .cost-row.has-edit { grid-template-columns: auto 1fr auto 2.75rem; }
}
/* Financials → a project's expenses figure opens its rows. */
.admin button.fin-job-exp {
  min-height: var(--ws-tap); padding: 0 0.35rem; margin: -0.6rem 0;
  font: inherit; font-size: 0.85rem; color: var(--text); background: none; border: 0;
  text-decoration: underline dotted; text-underline-offset: 0.2em; cursor: pointer;
}
.fin-exp-rows { flex-basis: 100%; list-style: none; margin: 0.35rem 0 0.2rem; padding: 0 0 0 0.75rem;
  border-left: 2px solid var(--border-strong); font-size: 0.85rem; }
.fin-exp-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center;
  gap: 0.1rem 0.75rem; padding: 0.25rem 0; border-bottom: 1px solid var(--border);
}
.fin-exp-row:last-child { border-bottom: 0; }
.fin-exp-what { min-width: 0; overflow-wrap: anywhere; }
.fin-exp-when { grid-column: 1; grid-row: 2; font-size: 0.78rem; }
.fin-exp-amt { grid-column: 2; grid-row: 1 / span 2; font-variant-numeric: tabular-nums; font-weight: 600; }
.fin-exp-row > .ws-act, .fin-exp-lock { grid-column: 3; grid-row: 1 / span 2; }

.fin-footnotes { margin: 1.25rem 0 0; font-size: 0.75rem; line-height: 1.5; }
.pexp-hint { margin: -0.35rem 0 0.6rem; font-size: 0.8rem; }

/* A phone reads the stack one period at a time rather than squeezing two money
   columns into a width neither fits. */
@media (max-width: 560px) {
  .fin-cards { grid-template-columns: 1fr; }
  .fin-pnl th, .fin-pnl td { padding: 0.3rem 0.25rem; font-size: 0.9rem; }
  .fin-job-name { flex-basis: 100%; }
}

.fin-box { margin-bottom: 0.5rem; }
.fin-box:last-child { margin-bottom: 0; }
.fin-empty { margin: 0; }

/* THE OVERDUE COUNT ON A CLOSED BAR. The one fact worth surfacing without
   opening the box, so it gets the copper the rest of the admin reserves for
   "this needs a look" — the same colour the planned-work strip uses for a dead
   estimate and .est-changes uses for a client waiting on Adam. Amber, not red:
   a late bill is a thing to do today, not a loss. */
.fin-chip-late { color: var(--copper-bright); }

.fin-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.fin-list[hidden] { display: none; }

/* One bill. A card rather than a table row: at 390px a five-column table is
   four columns of nothing, and this list is read on a phone as often as not. */
.fin-bill {
  padding: 0.7rem 0.85rem;
  background: var(--panel-2);
  border: 1px solid var(--border-strong);
  border-radius: 0;
}
/* Overdue takes a copper LEFT EDGE, not a fill. The same marker .est-line and
   the profit box already use for "this one", and it survives being one row of
   twenty in a way a background tint does not. */
.fin-bill.is-late { border-left: 3px solid var(--copper); }

/* Who and how much, on one line, with the money hard right — the question this
   page answers is "how much", and it should be scannable down a column even
   though the rows are cards. */
.fin-bill-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 0.5rem 1rem; flex-wrap: wrap;
}
.fin-bill-head[hidden] { display: none; }
.fin-bill-who { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; min-width: 0; }
.fin-bill-vendor { color: var(--text); font-size: 1rem; }
.fin-bill-desc { color: var(--muted); font-size: 0.9rem; overflow-wrap: anywhere; }
.fin-bill-amount {
  font-family: var(--font-display); font-weight: 700; color: var(--price);
  font-size: 1.05rem; font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* The dates and badges. Muted and small: they qualify the row, they aren't it. */
.fin-bill-facts {
  display: flex; align-items: center; gap: 0.35rem 0.6rem; flex-wrap: wrap;
  margin-top: 0.3rem; font-size: 0.85rem; color: var(--muted);
}
.fin-bill-facts[hidden] { display: none; }
.fin-bill-when { color: var(--muted); }

/* A rail INSIDE a card — separated by a hairline so it reads as "act on this
   row" rather than as more of the row's content. Same device as the job page's
   .jd-row-rail, same reason it isn't the same class. */
.fin-row-rail {
  display: flex; gap: 0.4rem; flex-wrap: wrap;
  margin-top: 0.55rem; padding-top: 0.5rem; border-top: 1px dashed var(--border-strong);
}
.fin-row-rail[hidden] { display: none; }

/* --- The two dialogs ------------------------------------------------------ */
/* What you are about to record, above the form that records it. A mark-paid on
   the wrong row is a correction in the books, so the subject is stated rather
   than assumed from whichever button was clicked. */
.pf-subject {
  margin: 0 0 0.9rem; padding: 0.6rem 0.8rem;
  background: var(--panel); border: 1px solid var(--border);
  border-left: 3px solid var(--copper); border-radius: var(--radius);
  line-height: 1.45;
}
.pf-subject strong { font-family: var(--font-display); color: var(--price); font-size: 1.1rem; }

/* WHAT THE VENDOR IS ABOUT TO BE TOLD. The notify tick is made once on the
   vendor form and then forgotten; this line is where its consequence becomes
   visible, at the moment it fires. Three states, and the middle one — ticked
   with no address on file — is the reason this exists at all: the gateway
   allows that combination deliberately (no email is ever fatal), so the screen
   has to be the thing that says so. */
.pf-notify { margin: 0.2rem 0 0.9rem; font-size: 0.88rem; color: var(--steel); }
.pf-notify--warn { color: var(--copper-bright); }
.bf-vendor-hint { margin: -0.4rem 0 0.9rem; font-size: 0.88rem; }

/* The notify checkbox on the VENDOR form (the Suppliers tab). Its hint is a
   whole sentence rather than a word, because this is the first control in the
   system that sends mail to someone outside the business and "Notify on
   payment" alone does not say who, when, or what happens without an address. */
.sf-notify-hint { display: block; margin-top: 0.15rem; color: var(--muted); font-size: 0.82rem; line-height: 1.45; }

@media (max-width: 34rem) {
  /* Per the theme's stated phone behaviour: rails go full width and stack, so
     a thumb gets the whole button rather than a third of one. */
  .ws-rail, .fin-row-rail { flex-direction: column; align-items: stretch; }
  .admin .ws-act { width: 100%; }
  /* The money keeps its own line under the vendor rather than being squeezed
     against a wrapped description. */
  .fin-bill-head { flex-direction: column; align-items: flex-start; gap: 0.25rem; }
}

/* --- Users screen (0050 Track 2) ------------------------------------------ */
/* The one admin screen whose subject is the admin itself. A disabled row is
   dimmed rather than hidden: "why can't they sign in" is a question the list
   should answer at a glance. */
.user-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.6rem; }
.user-card {
  padding: 0.75rem 0.9rem; background: var(--panel);
  border: 1px solid var(--border); border-radius: var(--radius);
}
.user-card.is-disabled { opacity: 0.62; }
.user-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.6rem; }
/* Wrapping now comes from the shared long-string rule near the top of the
   admin section — this screen had the only copy of the idea, as break-all. */
.user-you { font-size: 0.78rem; }
.user-facts { display: flex; flex-wrap: wrap; gap: 0.2rem 0.9rem; margin-top: 0.3rem; font-size: 0.85rem; }
.user-log-note { margin-top: 1rem; font-size: 0.85rem; }
.uf-note { font-size: 0.85rem; margin: 0 0 0.2rem; }
/* Set-password page: the invite acceptance form. */
#sp-form input[type="password"] {
  width: 100%; font: inherit; padding: 0.5rem 0.6rem; margin-bottom: 0.8rem;
  color: var(--text); background: var(--bg);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  min-height: 2.75rem;
}
#sp-form input[type="password"]:focus-visible { outline: 2px solid var(--copper); outline-offset: 1px; }

/* --- Close Out Job (0082) --------------------------------------------------
   The interview. One dialog, three regions in the order a person reads them:
   where the money stands, what to do about it, and what else is unfinished.

   Existing vocabulary only — .modal-panel--wide, .btn, .is-money/.is-over, the
   copper edge markers already used for "look at this one". Nothing new is
   invented for a dialog that exists to make existing mechanisms findable. */

/* THE ROLLUP. A grid rather than a flex row so the four figures line up in
   columns at every width — they are meant to be compared, and comparison needs
   a shared baseline. */
.close-rollup {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: 0.5rem 1rem; margin: 0 0 1rem;
  padding: 0.75rem 0.85rem; background: var(--panel);
  border: 1px solid var(--border); border-radius: var(--radius);
}
.close-rollup > div { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.close-rollup dt { color: var(--muted); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.06em; }
.close-rollup dd { margin: 0; font-family: var(--font-display); font-weight: 700; font-size: 1.05rem; }
/* OUTSTANDING IS THE GATE, so it is the one figure that is marked. The copper
   edge is the same "this is the one" marker .est-line and .fin-bill use. */
.close-rollup-gate { padding-left: 0.6rem; border-left: 3px solid var(--copper); }
.close-rollup-gate dd { font-size: 1.3rem; }

.close-head { margin: 1rem 0 0.5rem; font-size: 0.95rem; }
.close-head:first-child { margin-top: 0; }
.close-head-note { font-weight: 400; font-size: 0.85rem; }

.close-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.close-item {
  padding: 0.7rem 0.8rem; background: var(--panel);
  border: 1px solid var(--border); border-radius: var(--radius);
}
/* Ready to close wears the money colour on its edge; a stuck amendment wears
   the warning one. Both borrow markers already in the stylesheet. */
.close-item.is-ready { border-left: 3px solid var(--price); }
.close-item.is-stuck { border-left: 3px solid var(--copper-bright); }
/* "Or resolve it without collecting" — a hinge in the list, not another item
   of the same kind, so it is quieter and separated rather than boxed. */
.close-item.is-choice { background: none; border: none; padding: 0.6rem 0 0; border-top: 1px dashed var(--border-strong); border-radius: 0; }
.close-item.is-quiet { background: none; border: 1px dashed var(--border-strong); }
.close-item-head { margin: 0; color: var(--text); font-size: 0.95rem; }
.close-item-body { margin: 0.25rem 0 0; font-size: 0.85rem; }
.close-item-acts { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-top: 0.6rem; }
.admin .close-act { min-height: 2.75rem; font-size: 0.82rem; padding: 0.45rem 0.85rem; }
.admin .close-act:disabled { opacity: 0.45; cursor: not-allowed; }
.close-blocked { margin: 0; color: var(--muted); }

/* Where a branch lives when THIS page cannot perform it. Not a button, because
   a button that does nothing is the 0070 shape; a sentence that names the page
   it lives on is an answer. */
.close-elsewhere { display: block; margin-top: 0.3rem; }
.close-form-head { margin: 0 0 0.35rem; font-size: 1rem; }
.close-form-note { margin: 0 0 0.8rem; font-size: 0.88rem; }
.close-reversible { margin: 0.2rem 0 0.8rem; font-size: 0.85rem; }

/* The state a closed job's bar shows in place of the action it no longer has. */
.jd-bar-state {
  font-family: var(--font-display); font-size: 0.78rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted);
  padding: 0 0.5rem; white-space: nowrap;
}

/* The money panel's second door. A quiet link on its own row under
   Outstanding — the figure it acts on. */
.pnl-close-row { margin-top: 0.35rem; }
.pnl-close-row dd { font-weight: 400; }
.admin .pnl-close-link { font-size: 0.85rem; }

@media (max-width: 34rem) {
  .close-item-acts .close-act { width: 100%; }
}

/* --- Recurring expenses (0079) ---------------------------------------------
   The segmented control and the Repeating box. Existing tokens only, and the
   segment borrows the workstation frame's squared edges rather than inventing
   a shape. */

.bf-kind { border: 0; padding: 0; margin: 0 0 0.6rem; min-width: 0; }
.bf-kind-legend { padding: 0; margin-bottom: 0.3rem; }
.bf-mode-hint { margin: 0.4rem 0 0; font-size: 0.82rem; }

/* One control, two halves. A radiogroup rather than a pair of buttons: it is a
   choice between two states, and the keyboard already knows how to move through
   radios. The inputs stay in the DOM, visually hidden the accessible way — the
   .co-pick-hit lesson, never display:none. */
.seg { display: flex; border: 1px solid var(--border-strong); border-radius: 0; overflow: hidden; }
.seg-opt { flex: 1 1 0; min-width: 0; position: relative; }
.seg-opt input {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.seg-opt span {
  display: block; text-align: center; cursor: pointer;
  min-height: 2.75rem; line-height: 2.75rem; padding: 0 0.6rem;
  font-size: 0.88rem; color: var(--muted); background: var(--bg);
}
.seg-opt + .seg-opt span { border-left: 1px solid var(--border-strong); }
.admin .seg-opt span:hover { background: var(--panel-2); color: var(--text); }
.seg-opt input:checked + span { background: var(--copper); color: #14110E; font-weight: 600; }
.seg-opt input:focus-visible + span { outline: 2px solid var(--copper-bright); outline-offset: -2px; }

.bf-repeat { display: block; }
.bf-repeat[hidden] { display: none; }

/* A schedule row is a bill row that hasn't happened yet, so it reuses .fin-bill
   and only says what is different: paused reads back rather than gone. */
.fin-sched.is-paused { opacity: 0.6; }
.fin-sched.is-paused .fin-bill-amount { color: var(--muted); }

/* --- Void & duplicate, and the tax chip (0085) ------------------------------ */

/* THE TAX CHIP ON A COLLAPSED LINE BAR. Deliberately the SAME shape as
   `.ro-notax` — the read-only view's marker for the same fact — because two
   renderers of the same lines must not have two vocabularies. A reader who
   learns the pill in one view should recognise it in the other without being
   told, which is the whole reason it borrows every value rather than being
   "styled to match". */
/* 0099 — "Load older" under a merged client feed. Same control the Activity
   page uses (.act-more), reused rather than restyled. */
.act-list-slot .act-more { margin: 0.6rem 0 0; }

.el-chip-notax {
  font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--muted); border: 1px solid var(--border);
  border-radius: 999px; padding: 0.05rem 0.4rem; white-space: nowrap;
}

/* Void & duplicate. Sits under the signature block on a SENT contract, and
   nowhere else — see voidDuplicateSection for why its absence on a signed one
   is the feature. Quiet framing around a red button: the explanation has to be
   read before the action is taken, so the note leads and the button follows. */
.ct-revise { margin: 0 0 0.6rem; max-width: 34rem; }
.ct-revise-note { margin: 0 0 0.6rem; font-size: 0.88rem; }

/* --- Drag to reorder (0086) -------------------------------------------------

   THE HANDLE IS NOT A GRID CHILD, and that is deliberate rather than
   convenient. 0077's rule is that every child of the open .est-line gets an
   explicit column in all three regimes, because an unplaced one auto-places
   into whatever cell is free — that is the bug 0077 was reported for, and it
   cost a whole build to find.

   Adding a fifteenth child would have meant editing three column tables and
   shifting the identity row's columns in each. Instead the handle is taken OUT
   of the flow entirely: `position: absolute` makes it not a grid item at all,
   so no placement rule can apply to it and none has to. The row gains a
   left GUTTER equal to the handle's width, so every existing grid-column value
   still means exactly what it meant — the columns are computed inside the
   padding box and simply start further right.

   It is chrome ON the row, not a field IN it, and this says so structurally. */
.est-line { position: relative; padding-left: 2.1rem; }
.est-line--collapsed { position: relative; padding-left: 2.1rem; }

.admin .el-drag {
  position: absolute; left: 0; top: 0; bottom: 0; width: 2.1rem;
  display: flex; align-items: center; justify-content: center;
  padding: 0; margin: 0; min-height: 0;
  background: none; border: 0; border-right: 1px solid var(--border);
  border-radius: 0; cursor: grab; color: var(--muted);
  /* ⚠️ THE WHOLE TOUCH STORY IS THIS DECLARATION. A finger on the grip drags,
     because the browser is told this element consumes its own gestures; a
     finger anywhere else on the row scrolls the page as normal. No long-press
     timer, no guessing at intent, nothing to teach anyone. */
  touch-action: none;
}
.admin .el-drag:hover { background: var(--panel-2); color: var(--text); border-right-color: var(--copper); }
.admin .el-drag:active { cursor: grabbing; }

/* The grip, drawn with borders rather than a ⠿ glyph: glyphs render differently
   on every platform and this one has to sit still on a 2.75rem target. */
.el-grip {
  display: block; width: 0.55rem; height: 0.9rem;
  border-left: 2px dotted currentColor; border-right: 2px dotted currentColor;
}

/* The row being carried. Lifted, not hidden — you should still be able to read
   what you are moving. */
.est-line.is-dragging { opacity: 0.55; border-left-color: var(--copper-bright); }
.is-reordering { cursor: grabbing; }
/* No text selection anywhere in the list while a drag is in flight. */
.is-reordering, .is-reordering * { user-select: none; }

/* WHERE IT WILL LAND. A copper rule in the gap between rows, drawn with an
   outline on the neighbouring row so it needs no element of its own — an
   inserted placeholder node would be DOM the model does not know about, which
   is the one thing this build refuses to have. */
.est-line.is-drop-before::before,
.est-line.is-drop-after::after {
  content: ""; position: absolute; left: 0; right: 0; height: 3px;
  background: var(--copper); border-radius: 2px;
}
.est-line.is-drop-before::before { top: -0.4rem; }
.est-line.is-drop-after::after { bottom: -0.4rem; }

@media (max-width: 34rem) {
  /* Narrower gutter on a phone, where 2.1rem of a 390px row is real estate —
     but never below the 2.75rem touch target in height, which the absolute
     top/bottom already guarantees. */
  .est-line, .est-line--collapsed { padding-left: 1.7rem; }
  .admin .el-drag { width: 1.7rem; }
}

/* --- Tasks (0088 → 0130) ------------------------------------------------------
   0130 DELETED THE BOX AND THE WORKSPACE, and their rules with them (.tk-list,
   .tk-row, .tk-quick, .tk-views, .tk-body, .tk-note*, .tk-edit*). Nothing
   rendered them after 0120; see admin-tasks.js's header. What is left is the
   Tasks page's own pieces: the slot, the "probably about" chips (below), the
   Add-a-note form and the notes list, and the edit sheet. */
#tasks-slot { margin: 0 0 1.25rem; }

/* ADD A NOTE, on the task detail. The field leads and the button follows; on a
   phone the button takes its own line at full width. 16px so iOS does not zoom
   on focus. */
.task-note-form { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: flex-start; margin: 0 0 0.9rem; }
.admin #task-note-input { flex: 1 1 16rem; min-width: 0; min-height: 2.75rem; font-size: 1rem; }
.admin #task-note-save { flex: none; min-height: var(--ws-tap); }
.task-note-form .set-msg { flex-basis: 100%; }
@media (max-width: 34rem) {
  .admin #task-note-save { flex: 1 1 100%; }
}
/* THE NOTES, newest first, each with who wrote it — with two people writing
   them, a note without an author is half a sentence. */
.task-notes { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.task-note {
  padding: 0.6rem 0.75rem; background: var(--panel);
  border: 1px solid var(--border); border-radius: var(--radius); min-width: 0;
}
.task-note-body { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.task-note-who { margin: 0.25rem 0 0; font-size: 0.82rem; }

/* THE EDIT SHEET. Two fields, stacked, full width, 16px. */
.te-body { display: grid; gap: 0.35rem; align-content: start; }
.admin #te-title, .admin #te-who { width: 100%; min-height: var(--ws-tap); font-size: 1rem; }
.te-foot { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
.admin .te-foot .btn { min-height: var(--ws-tap); }

/* --- THE SERVICE CALL MODAL (0106) ---------------------------------------
   Theme vocabulary only. The line editor inside it brings its own dress
   (.est-line and the whole 0077 grid), so what is here is the frame around it:
   four short rows of fields and the fork.

   ⚠️ THE SEGMENTED CONTROL IS A RADIO GROUP WEARING BUTTONS. Two mutually
   exclusive answers that change what the form BUILDS deserve to look like a
   choice rather than a toggle — a checkbox reads as "and also", and this is
   "instead of". */
.sc-form { display: grid; gap: 0.2rem; }
.sc-h {
  margin: 0.9rem 0 0.35rem; font-family: var(--font-display);
  font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted);
}
.sc-row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.6rem; }
.sc-f { display: flex; flex-direction: column; gap: 0.25rem; flex: 1 1 11rem; min-width: 0; }
.sc-f--days { flex: 0 1 5rem; }
.sc-f--wide { flex: 1 1 100%; }
.admin .sc-f input, .admin .sc-f select { min-height: 2.75rem; font-size: 1rem; }
.admin .sc-new-client { min-height: 2.75rem; flex: none; }
.sc-lines { display: grid; gap: 0.5rem; margin-bottom: 0.4rem; }

.sc-seg { display: inline-flex; flex-wrap: wrap; gap: 0; }
.admin .sc-seg-btn {
  min-height: 2.75rem; padding: 0.45rem 1rem;
  background: var(--bg); color: var(--muted);
  border: 1px solid var(--border-strong); border-radius: 0;
  font-size: 0.9rem; cursor: pointer;
}
.admin .sc-seg-btn + .sc-seg-btn { border-left: 0; }
.admin .sc-seg-btn:hover { background: var(--panel-2); color: var(--text); }
.admin .sc-seg-btn.is-on {
  background: var(--copper); border-color: var(--copper); color: #14110E; font-weight: 600;
}
.admin .sc-seg-btn:focus-visible { outline: 2px solid var(--copper); outline-offset: 1px; }

/* The free fork. Indented and ruled so it reads as a consequence of the answer
   above it rather than as a fifth question of equal weight. */
.sc-fork {
  margin: 0.6rem 0 0; padding: 0.7rem 0 0.2rem 0.8rem;
  border-left: 2px solid var(--border-strong);
  display: grid; gap: 0.5rem; justify-items: start;
}
.sc-ask { margin: 0; color: var(--text); font-size: 0.95rem; }
.sc-note { margin: 0; font-size: 0.88rem; }
.sc-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin-top: 1rem; }

/* ⚠️ WHERE A PARTIAL FAILURE IS NAMED, and it is styled to be READ rather than
   dismissed: copper edge, panel ground, its own block under the actions. It is
   not an error — something real was created — so it must not wear the error
   red that means "nothing happened". */
.sc-stopped {
  margin-top: 0.8rem; padding: 0.7rem 0.85rem;
  background: var(--panel); border: 1px solid var(--border-strong);
  border-left: 3px solid var(--copper); border-radius: var(--radius);
  display: grid; gap: 0.3rem; font-size: 0.92rem;
}
/* The [hidden] trap, a fourth time: display:grid above beats the UA rule, so
   the empty stop box rendered as a bordered strip on every open. */
.sc-stopped[hidden] { display: none; }
/* In the pinned footer (0123) the actions sit directly under the stop box. */
.ws-modal-foot .sc-actions { margin-top: 0; }
.ws-modal-foot .sc-stopped { margin: 0 0 0.6rem; }
.sc-stopped-h { margin: 0; color: var(--text); font-weight: 600; }
.sc-stopped p { margin: 0; }

@media (max-width: 34rem) {
  /* One field per line: a date and a day-count sharing a phone row left the
     date field too narrow to read the value already in it. */
  .sc-f, .sc-f--days { flex: 1 1 100%; }
  .admin .sc-seg-btn { flex: 1 1 auto; }
}

/* --- the suggestion chips (0090) ---
   ⚠️ THESE ARE GUESSES AND THEY MUST LOOK LIKE GUESSES. The gateway matched a
   name in dictated text; it did not learn anything. So: DASHED, never solid —
   the same borrowed grammar .ws-box--muted uses for "present but not
   asserted" — quiet steel rather than the copper every real action on this
   screen wears, and small enough to skim past. A chip styled like a fact would
   be making a claim the matcher never made, and the first wrong one would
   teach Adam to distrust the right ones.

   Its own line, full width of the row, so it never competes with the title for
   the first thing read. */
.tk-chips {
  flex: 1 1 100%; margin: 0.35rem 0 0;
  display: flex; align-items: center; gap: 0.3rem; flex-wrap: wrap;
}
.tk-chips-lead { color: var(--muted); font-size: 0.74rem; letter-spacing: 0.03em; }
.admin .tk-chip {
  display: inline-flex; align-items: center; min-height: 1.9rem;
  padding: 0.15rem 0.5rem;
  color: var(--steel); font-size: 0.78rem; line-height: 1.3;
  border: 1px dashed var(--border-strong); border-radius: 0;
  text-decoration: none;
}
.admin .tk-chip:hover {
  color: var(--text); border-color: var(--steel); border-style: solid;
}
.admin .tk-chip:focus-visible { outline: 2px solid var(--copper); outline-offset: 1px; }
/* On the task detail (0130) a chip is a link somebody taps on a phone, so it
   clears the touch minimum there. */
.admin .ws-section .tk-chip { min-height: var(--ws-tap); }
/* 0140: stacked detail sections (a task, a service call, a contractor) were
   112px apart on the page-band rule. A section gap is enough. */
.ws-section { margin: 0 0 var(--ws-gap-lg); }
.ws-section:last-child { margin-bottom: 0; }


/* --- PROJECTS (0132b) -----------------------------------------------------------
   The admin gallery editor. Page prefix pj-, photo lanes pp-, the dialog pm-.
   Every control clears --ws-tap; every input is 16px (no iOS zoom); nothing is
   wider than its column, so the body never scrolls sideways (plan §4). */

/* The list's placeholder when a project has no photo yet. */
.pj-nothumb { display: block; width: 100%; height: 100%; background: repeating-linear-gradient(135deg, var(--panel-2) 0 6px, var(--panel) 6px 12px); }

/* Detail sections: a block with a gap under it. */
.pj-sec { margin: 0 0 1.5rem; min-width: 0; }
.pj-desc { margin: 0.6rem 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.pj-missing { color: var(--copper-bright); font-weight: 600; }
.pj-confirm:empty { display: none; }
.pj-note { margin: 0.75rem 0 0; padding: 0.55rem 0.75rem; border-left: 3px solid var(--copper); background: var(--panel); }
.admin .pj-slot { width: 100%; max-width: 28rem; min-height: var(--ws-tap); font-size: 1rem; }

/* THE PUBLISH CHECKLIST — what's missing, as a list a person reads, not an
   error string. Copper, not error red: nothing is broken, something is to do. */
.pj-checklist {
  margin: 0.75rem 0 0; padding: 0.75rem 0.9rem;
  border: 1px solid var(--copper); border-radius: var(--radius); background: var(--panel);
}
.pj-checklist-h { margin: 0 0 0.4rem; }
.pj-checklist ul { margin: 0; padding-left: 1.1rem; display: grid; gap: 0.35rem; }
.pj-checklist li { overflow-wrap: anywhere; }

/* THE DIALOG. Fields stacked; City and ZIP share a row from 34rem. */
.pm-body { display: grid; gap: 0.35rem; align-content: start; }
.admin .pm-body input[type="text"], .admin .pm-body select, .admin .pm-body textarea {
  width: 100%; min-height: var(--ws-tap); font-size: 1rem;
}
.pm-hint { margin: 0 0 0.5rem; font-size: 0.85rem; }
.pm-warn { color: var(--text); padding: 0.45rem 0.6rem; border-left: 3px solid var(--copper); background: var(--panel); }
.pm-row { display: grid; grid-template-columns: 1fr; gap: 0.35rem 0.75rem; }
@media (min-width: 34rem) { .pm-row { grid-template-columns: 1fr 9rem; } }
.pm-field { display: grid; gap: 0.35rem; min-width: 0; }
.pm-consent { border: 1px solid var(--border); border-radius: var(--radius); padding: 0.6rem 0.75rem; margin: 0.5rem 0 0; }
.pm-check { display: flex; gap: 0.6rem; align-items: center; min-height: var(--ws-tap); cursor: pointer; }
.admin .pm-check input { width: 1.4rem; height: 1.4rem; flex: none; }
.pm-foot, .te-foot { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
.admin .pm-foot .btn { min-height: var(--ws-tap); }

/* THE PHOTO LANES. One column on a phone; Before and After side by side from
   46rem, which is the pairing the public page shows (D10). */
.pp-notice {
  margin: 0 0 0.75rem; padding: 0.7rem 0.85rem; border-radius: var(--radius);
  border: 1px dashed var(--border-strong); background: var(--panel);
}
.pp-hint { margin: 0 0 0.75rem; font-size: 0.88rem; }
.pp-lanes { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; }
@media (min-width: 46rem) { .pp-lanes { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
.pp-lane { min-width: 0; display: grid; gap: 0.6rem; align-content: start; }
.pp-lane-h { margin: 0; font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.95rem; }
.pp-empty { margin: 0; }
.pp-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.75rem; }
.pp-card {
  display: grid; gap: 0.4rem; min-width: 0; padding: 0.6rem;
  border: 1px solid var(--border); border-radius: var(--radius); background: var(--panel);
}
.pp-thumb { width: 100%; max-width: 20rem; height: auto; aspect-ratio: 4 / 3; object-fit: cover; border-radius: calc(var(--radius) - 2px); background: var(--panel-2); }
.pp-pos { margin: 0; font-size: 0.85rem; color: var(--muted); }
.admin .pp-alt { width: 100%; min-height: var(--ws-tap); font-size: 1rem; }
.pp-acts { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.admin .pp-act { min-height: var(--ws-tap); min-width: var(--ws-tap); padding: 0.45rem 0.7rem; }
/* The file input lives inside its label, which is the visible button. */
.pp-file { position: absolute; width: 1px; height: 1px; opacity: 0; overflow: hidden; }
.admin .pp-add, .admin .pp-replace { position: relative; display: inline-flex; align-items: center; justify-content: center; min-height: var(--ws-tap); cursor: pointer; }
.admin .pp-add.is-disabled, .admin .pp-replace.is-disabled { opacity: 0.5; cursor: not-allowed; }
.pp-confirm:empty { display: none; }

/* THE QUEUE — one line per file in flight, with its own progress bar. */
.pp-queue { list-style: none; margin: 0.75rem 0 0; padding: 0; display: grid; gap: 0.4rem; }
.pp-queue:empty { display: none; }
.pp-q {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0.25rem 0.6rem; align-items: center;
  padding: 0.5rem 0.7rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--panel);
}
.pp-q-name { min-width: 0; overflow-wrap: anywhere; }
.pp-q-state { grid-column: 1 / -1; font-size: 0.85rem; color: var(--muted); overflow-wrap: anywhere; }
.pp-q-bar { grid-column: 1 / -1; width: 100%; height: 0.5rem; }
.pp-q.is-failed { border-color: var(--copper); }
.pp-q.is-failed .pp-q-state { color: var(--copper-bright); }
.admin .pp-retry { min-height: var(--ws-tap); }

/* ═══════════════════════════════════════════════════════════════════════════
   THE DOCUMENT A CLIENT KEEPS (0137a)
   ═══════════════════════════════════════════════════════════════════════════
   The banner above a signed agreement, and the row of things a client can do
   with it. Both signing pages use these; /invoice/ and /estimate/ inherit them
   when 0137c/d put the same block on those pages. */
.doc-signed {
  border-left: 3px solid var(--copper);
}
.doc-actions {
  display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center;
  margin: 1.5rem 0 0.5rem;
}
.doc-actions .btn { min-height: 2.75rem; }
.doc-actions-note {
  flex-basis: 100%; margin: 0.4rem 0 0; color: var(--muted); font-size: 0.92rem;
}
.doc-ask-line {
  flex-basis: 100%; margin: 0.75rem 0 0; color: var(--muted); font-size: 0.95rem;
}
.doc-ask { color: var(--copper); }

/* 0150 — the deposit under a signed contract: one clear next step. */
.sc-deposit { margin: 0 0 1.25rem; display: grid; gap: 0.5rem; justify-items: start; }
.sc-deposit[hidden] { display: none; }
.sc-deposit p { margin: 0; }
.sc-deposit .btn { min-height: 2.75rem; }
@media print { .sc-deposit { display: none; } }
.doc-ask:hover { color: var(--copper-bright); }

/* The account an invoice settles (0140) — the working behind the balance. */
.ip-account {
  margin: 0 0 1.25rem; padding: 1rem 1.1rem;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
}
.ip-acct-h {
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.06em; font-size: 0.8rem; color: var(--muted);
  margin: 0 0 0.75rem;
}
.ip-acct-line {
  display: flex; justify-content: space-between; gap: 1rem;
  margin: 0 0 0.35rem; font-size: 0.95rem; color: var(--text);
}
/* ⚠️ min-width:0 and overflow-wrap on the LABEL. A discount reason is Adam's
   free text and a change-order title is a client's words; either can be long
   enough to push the page sideways at 390 without this. */
.ip-acct-line > span:first-child { min-width: 0; overflow-wrap: anywhere; color: var(--muted); }
.ip-acct-line > span:last-child { white-space: nowrap; }
.ip-acct-line.is-sub {
  border-top: 1px solid var(--border); padding-top: 0.5rem; margin-top: 0.5rem;
}
.ip-acct-line.is-sub > span { color: var(--text); font-weight: 600; }
.ip-acct-line.is-total {
  border-top: 1px solid var(--border-strong); padding-top: 0.6rem; margin-top: 0.6rem;
}
.ip-acct-line.is-total > span { color: var(--price); font-weight: 700; font-size: 1.05rem; }

/* The invoice as a document (0137c): who it's for, and the itemisation that
   used to exist only inside the PDF. */
.ip-for {
  font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.04em; font-size: 0.9rem; color: var(--muted);
  margin: 0 0 0.75rem;
}
.ip-itemisation {
  margin: 0 0 1.25rem; padding: 1rem 1.1rem;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
}
/* ⚠️ THE LINE BREAKS ARE THE CONTENT. Each line of the itemisation is its own
   paragraph and a blank line is a gap — a client reads one charge per line, the
   way it was typed. Collapsing them is what put "$150.00?* Bricks - $95.00" in
   front of a customer (0136a). `overflow-wrap: anywhere` because a pasted
   reference or a long description must not push the page sideways at 390. */
.ip-itemisation p {
  margin: 0 0 0.3rem; color: var(--text); font-size: 0.95rem; line-height: 1.5;
  overflow-wrap: anywhere;
}
.ip-itemisation p:last-child { margin-bottom: 0; }
.ip-terms { margin: 1rem 0 0; font-size: 0.82rem; line-height: 1.45; color: var(--muted); }
.ip-itemisation .ml-gap { height: 0.5rem; margin: 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   PRINT (0137b) — a page a client can file
   ═══════════════════════════════════════════════════════════════════════════
   The five token pages a client opens from an email: /estimate/, /invoice/,
   /sign-contract/, /sign-change-order/, /my-booking/. Before this the
   stylesheet had ZERO print rules, so a signed contract printed as the first
   screenful of its terms — `.sign-terms` is a scroll box — under a site header,
   in near-white text, followed by the whole service-area footer.

   ⚠️ SCOPED BY A CLASS, NOT BY :has(). `doc-page` is put on <body> by
   base.njk from the page's own front matter. 0137a's stopgap keyed the chrome
   rules off `body:has(#sc-agreement)`, which made a browser's selector support
   the thing deciding whether a contract's terms reached the paper. A class is
   decided at build time and cannot be unsupported.

   ⚠️ NOTHING HERE PRINTS BY DEFAULT ON ANY OTHER PAGE. Every rule is prefixed
   with .doc-page, so the marketing site prints exactly as it did. */

/* The identity block exists in the DOM always and is shown only on paper. */
.print-only { display: none; }

@media print {
  @page { margin: 16mm; }

  /* ── 1. INK ON PAPER ─────────────────────────────────────────────────────
     The site is dark-on-dark and browsers drop backgrounds by default, which
     without this prints near-white text onto white paper. Colour is forced
     rather than merely re-declared, because the tokens are inherited from
     :root and would otherwise win on specificity. */
  .doc-page {
    background: #fff !important;
    color: #000 !important;
  }
  .doc-page main,
  .doc-page main * {
    background: transparent !important;
    box-shadow: none !important;
    color: #000 !important;
  }
  /* Secondary text stays secondary — readable grey, not invisible. */
  .doc-page .muted,
  .doc-page .print-id-meta,
  .doc-page .sign-subline,
  .doc-page .ev-subline,
  .doc-page .sign-notes,
  .doc-page .ev-notes,
  .doc-page .sign-line-unit,
  .doc-page .ev-line-unit,
  .doc-page .sign-terms p {
    color: #333 !important;
  }
  /* A URL on paper is not clickable, so a link reads as text with a rule. */
  .doc-page a { color: #000 !important; text-decoration: underline; }

  /* ── 2. NOTHING CLIPS ────────────────────────────────────────────────────
     Deliberately a sweep rather than a list. `.sign-terms` is the one scroll
     box on these pages today (26rem, overflow-y: auto — the whole legal text
     of a contract sat behind it), and the next one added must not need this
     file edited again to reach the paper. */
  .doc-page main,
  .doc-page main * {
    max-height: none !important;
    overflow: visible !important;
  }
  .doc-page .sign-terms {
    border: 0 !important;
    padding: 0 !important;
  }

  /* ── 3. CHROME AND CONTROLS OFF ──────────────────────────────────────────
     Anything a reader cannot act on from paper. Buttons, forms, the nav and
     the whole multi-column service-area footer. */
  .doc-page .site-header,
  .doc-page .site-footer,
  .doc-page .fs-intro,
  .doc-page .doc-actions,
  .doc-page .hero-actions,
  .doc-page .btn,
  .doc-page button,
  .doc-page form,
  .doc-page textarea,
  .doc-page input,
  .doc-page select,
  .doc-page label,
  .doc-page .sign-form,
  .doc-page #sc-form-wrap,
  .doc-page .ev-alt,
  .doc-page .ev-alt-row,
  .doc-page .ev-thumbs,
  .doc-page .ev-alt-form,
  .doc-page .section-eyebrow,
  .doc-page .lightbox,
  /* Prose that only makes sense next to a control that isn't printed —
     "You'll be taken to Stripe", "Request a pickup and we'll…". Marked in the
     template rather than guessed at by selector. */
  .doc-page .no-print,
  /* The estimate's intro is an instruction — "approve at the bottom" — and the
     bottom is not printed. The document below it already says who it is for.
     The signing pages' intros are statements, not instructions, and stay. */
  .doc-page #ev-intro,
  /* The page's own <h1> is a screen instruction ("Review & sign"), which is a
     strange thing to head a signed contract. The identity block is the title
     on paper. */
  .doc-page .page-head h1 {
    display: none !important;
  }

  /* ── 4. WHAT THE SHEET SAYS IT IS ────────────────────────────────────────
     Print-only, so a filed page stands alone without the browser's header. */
  /* ⚠️ :not([hidden]) — WITHOUT IT THIS !important BEATS THE hidden ATTRIBUTE,
     and a print-only line the page deliberately hid would print anyway. 0137e
     hit exactly that: "Weight ticket: available on your booking page" would
     have printed on every booking that has no ticket. */
  .doc-page .print-only:not([hidden]) { display: block !important; }
  .doc-page .print-id {
    margin: 0 0 1.25rem;
    padding: 0 0 0.75rem;
    border-bottom: 1px solid #000 !important;
    break-after: avoid; page-break-after: avoid;
  }
  .doc-page .print-id-brand {
    margin: 0 0 0.35rem; font-size: 10pt; letter-spacing: 0.06em; text-transform: uppercase;
  }
  /* WO-0023 — the logo above the brand line, when the build has one. */
  .doc-page .print-id-logo {
    display: block; width: auto; height: 0.6in; max-width: 3in; margin: 0 0 0.35rem;
  }
  .doc-page .print-id-title { margin: 0 0 0.25rem; font-size: 18pt; }
  .doc-page .print-id-meta { margin: 0; font-size: 10pt; }

  /* ── 5. PAGE BREAKS ──────────────────────────────────────────────────────
     A line item, a total or a signature block split across a page boundary is
     how a printed figure becomes ambiguous.

     NOTE: there is no <table> on any of these five pages — every money layout
     is a CSS grid row — so there are no table headers to repeat. If a table is
     ever introduced here, `thead { display: table-header-group }` is the rule
     it will need, and it is not written speculatively. */
  .doc-page .sign-line,
  .doc-page .ev-line,
  .doc-page .ip-line,
  .doc-page .sign-subline,
  .doc-page .ev-subline,
  .doc-page .sign-total,
  .doc-page .ev-total,
  .doc-page .sign-notes,
  .doc-page .ev-notes,
  .doc-page .doc-signed,
  .doc-page .notice {
    break-inside: avoid; page-break-inside: avoid;
  }
  .doc-page h1, .doc-page h2, .doc-page h3,
  .doc-page .sign-h, .doc-page .sign-term-h, .doc-page .print-id-title {
    break-after: avoid; page-break-after: avoid;
  }
  .doc-page .sign-terms { orphans: 3; widows: 3; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   0134 — CONTRACTOR RECRUITING
   Admin: the Contractors page's hiring section and review dialogs, and the
   dashboard's Hiring strip. Public: /work-with-us/ and /work-with-us/hire/.
   ═══════════════════════════════════════════════════════════════════════════ */

/* --- admin: hiring requests on the Contractors page ------------------------ */
.kc-hiring { margin: 0 0 1.25rem; min-width: 0; }
.kc-hiring-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 0.5rem 1rem; flex-wrap: wrap; border-bottom: 1px solid var(--border); margin: 0 0 0.75rem; }
.kc-hiring-head .admin-h { border-bottom: 0; margin: 0; padding-bottom: 0.4rem; }
.kc-hiring-head .btn { min-height: var(--ws-tap); margin-bottom: 0.4rem; }
.kc-hires { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.kc-hire {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem 1rem; flex-wrap: wrap;
  padding: 0.6rem 0.75rem; border: 1px solid var(--border); border-left: 3px solid var(--copper);
  border-radius: var(--radius); background: var(--panel); min-width: 0;
}
.kc-hire-text { display: grid; gap: 0.15rem; min-width: 0; flex: 1 1 14rem; }
.kc-hire-title { font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.03em; }
.kc-hire-headline, .kc-hire-meta { overflow-wrap: anywhere; }
.kc-hire-meta { font-size: 0.85rem; }
.kc-new { color: var(--copper-bright); }
.kc-hire-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.kc-hire-btn { min-height: var(--ws-tap); }
.kc-filled { margin: 0.75rem 0 0; }
.kc-filled > summary { cursor: pointer; min-height: var(--ws-tap); display: flex; align-items: center; color: var(--muted); }
.kc-filled .kc-hire { border-left-color: var(--border-strong); }
.kc-note { margin: 0.75rem 0 0; padding: 0.55rem 0.75rem; border-left: 3px solid var(--copper); background: var(--panel); overflow-wrap: anywhere; }
.kc-note[hidden] { display: none; }
.kc-src { text-transform: lowercase; }
/* --- admin: the detail ------------------------------------------------------ */
.kc-self-note { margin: 0 0 0.75rem; }
.kc-sub { font-size: 0.95rem; margin: 1rem 0 0.35rem; }
.kc-rate-text { max-width: 62ch; }
/* --- admin: the dialogs ----------------------------------------------------- */
.kc-links { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.kc-link { min-height: var(--ws-tap); display: inline-flex; align-items: center; overflow-wrap: anywhere; }
.kc-launched { margin: 0 0 0.5rem; }
.kc-pair { grid-template-columns: 1fr; }
@media (min-width: 40rem) { .kc-pair { grid-template-columns: 1fr 1fr; } }

/* WORK AND PAY (0146) — what Adam owes this sub, on this sub's page only.
   Deliberately the SAME row shape as the Financials project list (.fin-job): a
   wrapping flex row of tabular figures, so the one screen showing a payable per
   job and the one showing it per sub read the same way. Copied rather than
   shared because the two pages share no stylesheet section and the rule set is
   six lines; if a third screen wants it, that is when it moves. */
.kc-work-totals {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2rem 1.25rem;
  margin: 0 0 0.75rem; font-size: 1.05rem; font-variant-numeric: tabular-nums;
}
.kc-work-total-owed { font-weight: 600; }
.kc-work-list { list-style: none; margin: 0; padding: 0; }
.kc-work-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.9rem;
  padding: 0.5rem 0; border-bottom: 1px solid var(--border);
}
.kc-work-row:last-child { border-bottom: 0; }
.kc-work-title { flex: 1 1 12rem; font-weight: 600; overflow-wrap: anywhere; }
.kc-work-when { font-size: 0.85rem; color: var(--muted); white-space: nowrap; }
.kc-work-fig { font-size: 0.9rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
.kc-work-k { color: var(--muted); font-size: 0.85rem; }
/* The same copper Lena gave the Financials column, for the same reason: money
   still outstanding, not a cost already borne. */
.kc-work-owed { color: var(--copper-bright); }
/* THE LINE THAT KEEPS THE TOTAL HONEST. Small, but never hidden and never
   folded — a total with a silent exclusion is one somebody disputes later. */
.kc-work-excluded { margin: 0.75rem 0 0; font-size: 0.85rem; }
/* 0135. Money that went out against this unit but NOT to this sub — the
   double-pay guard. Deeper --copper, not the payable's --copper-bright: same
   family, because it is the same subject, and a different weight, because it is
   not a figure Adam owes. It is the one he needs to have read before he pays
   the row beside it. No new palette token for one line. */
.kc-work-elsewhere { color: var(--copper); font-weight: 600; }
.kc-work-moved { margin: 0.4rem 0 0; font-size: 0.85rem; }
/* WO-0080 — credit on account: the history under the work, newest first. */
.kc-credit { margin: 0.9rem 0 0; display: grid; gap: 0.35rem; }
.kc-credit-h { margin: 0; font-weight: 700; font-size: 0.95rem; }
.kc-credit-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.45rem; font-size: 0.9rem; }
.kc-credit-row { display: flex; flex-wrap: wrap; gap: 0.25rem 0.6rem; align-items: baseline; min-width: 0; }
.kc-credit-what { min-width: 0; overflow-wrap: anywhere; }
.kc-credit-why { flex-basis: 100%; font-size: 0.85rem; overflow-wrap: anywhere; }
.kc-credit-row.is-reversed .kc-credit-what { text-decoration: line-through; text-decoration-thickness: 1px; }
.kc-credit-row.is-reversed .kc-credit-what .muted { text-decoration: none; }
.admin .kc-credit-take { min-height: 2.75rem; }
.kc-credit-back { flex-basis: 100%; display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: flex-end; }
.kc-credit-back .inv-label { flex: 1 1 14rem; min-width: 0; }
.kc-credit-back input { font-size: 1rem; min-height: 2.75rem; width: 100%; box-sizing: border-box; }
.admin .kc-credit-back .btn { min-height: 2.75rem; }
.kc-work-wait, .kc-work-none { margin: 0; }
.kc-work-dead { margin: 0; }
/* A phone gives the title its own line rather than squeezing three money
   figures against it — the same break .fin-job-name takes at the same width. */
@media (max-width: 560px) {
  .kc-work-title { flex-basis: 100%; }
}
.kv-checks { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.kv-check { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 0.5rem 0; border-bottom: 1px solid var(--border); }
.kv-check-text { display: grid; gap: 0.1rem; min-width: 0; }
.kv-btn { min-height: var(--ws-tap); min-width: 6rem; }
/* --- admin: the dashboard's Hiring strip ------------------------------------ */
.dash-hire-new { color: var(--copper-bright); font-weight: 600; }

/* --- public: /work-with-us/ and the hire page ------------------------------- */
.wwu-pitch { display: grid; gap: 0.75rem; margin: 1.25rem 0 1rem; padding: 0; list-style: none; }
.wwu-pitch li { padding-left: 1.4rem; position: relative; }
.wwu-pitch li::before { content: "✓"; position: absolute; left: 0; color: var(--copper); font-weight: 700; }
.wwu-form fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
.wwu-yn { margin: 1rem 0 0; }
/* The legend reads like every other field label on the form (base `label`). */
.wwu-yn legend {
  margin: 0 0 0.35rem; padding: 0; font-family: var(--font-display); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.9rem; color: var(--steel);
}
.wwu-yn-opts { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.wwu-yn-opt {
  margin: 0; font-family: inherit; text-transform: none; letter-spacing: normal; font-size: 1rem; color: var(--text);
  display: inline-flex; align-items: center; gap: 0.5rem; min-height: 2.75rem; padding: 0 1rem;
  border: 1px solid var(--border-strong); border-radius: var(--radius); cursor: pointer; background: var(--panel);
}
.wwu-yn-opt:has(input:checked) { border-color: var(--copper); color: var(--copper-bright); }
.wwu-yn-opt input { margin: 0; accent-color: var(--copper); }
.wwu-licence { margin: 0.5rem 0 0; }
.wwu-licence[hidden] { display: none; }
.hire-card { margin: 0 0 1.5rem; padding: 1.25rem; border: 1px solid var(--border); border-left: 3px solid var(--copper); border-radius: var(--radius); background: var(--panel); }
.hire-card h2 { margin: 0 0 0.35rem; }
.hire-trade { font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.08em; color: var(--copper); font-size: 0.9rem; margin: 0 0 0.35rem; }
.hire-facts { display: grid; grid-template-columns: max-content 1fr; gap: 0.35rem 1rem; margin: 0.75rem 0 0; }
.hire-facts dt { color: var(--muted); }
.hire-facts dd { margin: 0; overflow-wrap: anywhere; }
.hire-details { margin: 0.75rem 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.hire-filled { border-left-color: var(--border-strong); }
.hire-card[hidden] { display: none; }
/* The "Do you do great work?" line under the page CTAs. Quiet on purpose: it
   sits beside the page's own call to action and must not compete with it. */
.wwu-cta { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0.5rem 0.75rem; padding: 1.25rem 0; text-align: center; border-top: 1px solid var(--border); }
.wwu-cta-lead { color: var(--muted); }
.wwu-cta a { color: var(--copper); font-weight: 600; text-decoration: none; }
.wwu-cta a:hover { color: var(--copper-bright); }

/* ---------------------------------------------------------------------------
   THE BELL (0147) — a badge in the nav on every admin page, and the list
   behind it. Sits in .admin-nav-user beside the gear; the panel floats over
   the page from tablet up and is a full-width sheet under the top bar on a
   phone, where a 22rem dropdown would hang off the left edge.
   ------------------------------------------------------------------------- */
.admin-bell { position: relative; }
.admin-bell[hidden] { display: none; }
.admin-bell-btn {
  position: relative; cursor: pointer; background: none;
  padding-left: 0.6rem; padding-right: 0.6rem; justify-content: center;
}
.admin-bell-btn[aria-expanded="true"] { color: var(--text); border-color: var(--border-strong); }
/* The count. Copper, because it is the one thing on the bar asking for
   attention — the palette's rule for copper, not decoration. */
.admin-bell-badge {
  position: absolute; top: 0.2rem; right: 0.1rem;
  min-width: 1.1rem; height: 1.1rem; padding: 0 0.25rem;
  border-radius: 999px; background: var(--copper); color: #14110E;
  font-family: var(--font-body); font-size: 0.66rem; font-weight: 700;
  line-height: 1.1rem; text-align: center; letter-spacing: 0;
}
.admin-bell-badge[hidden] { display: none; }
/* SCROLL STAYS IN THE LIST (WO-0039). The panel was the scroller and nothing
   contained it, so a swipe that reached either end of the list — or any swipe
   on a list too short to scroll — moved the page behind it. Now the panel is a
   column that never scrolls; the LIST is the scroller, with overscroll
   contained, and admin-bell.js stops the swipes CSS can't (a list with nothing
   to scroll, a drag on the header). No overflow:hidden on the body: that loses
   the reader's place when the panel closes. */
.admin-bell-panel {
  position: absolute; z-index: 30; top: calc(100% + 0.3rem); right: 0;
  width: min(24rem, calc(100vw - 2rem)); max-height: min(32rem, 75vh); max-height: min(32rem, 75dvh);
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--panel-2); border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
  overscroll-behavior: contain;
}
.admin-bell-panel[hidden] { display: none; }
.bell-head {
  flex: none; display: flex; align-items: center; justify-content: space-between;
  padding: 0.4rem 0.7rem; border-bottom: 1px solid var(--border);
}
.bell-head-title {
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.04em; font-size: 0.82rem; color: var(--text);
}
.bell-settings { font-size: 0.8rem; padding: 0.3rem 0; }
.bell-list {
  list-style: none; margin: 0; padding: 0;
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.bell-item + .bell-item { border-top: 1px solid var(--border); }
/* COMPACT (WO-0039): title and time share a line, the body sits under them.
   Real notifications are a short title ("Payment received") and one short
   line ("$2,188.30 from Julia · INV-0000033"), so two lines carry all of it.
   The row stays a full 44px tap target. */
.bell-link {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: baseline;
  column-gap: 0.6rem; row-gap: 0.05rem;
  padding: 0.4rem 0.7rem 0.4rem 0.6rem;
  color: var(--steel); text-decoration: none; border-left: 3px solid transparent;
  min-height: var(--ws-tap); line-height: 1.3;
}
.bell-link:hover { background: var(--panel); color: var(--text); }
.bell-item.is-unread .bell-link { border-left-color: var(--copper); }
.bell-title {
  color: var(--text); font-weight: 600; font-size: 0.86rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bell-item:not(.is-unread) .bell-title { font-weight: 500; }
.bell-when { font-size: 0.72rem; white-space: nowrap; grid-column: 2; grid-row: 1; }
.bell-body { grid-column: 1 / -1; font-size: 0.8rem; overflow-wrap: anywhere; }
.bell-empty { padding: 0.75rem 0.7rem; font-size: 0.84rem; }
@media (max-width: 45.99rem) {
  .admin-bell { position: static; }
  /* A sheet under the top bar, ending above the tab bar. dvh, not vh: on a
     phone vh is the LARGEST viewport, so the list's last rows sat under the
     browser's toolbar and the only way to reach them was to scroll the page. */
  .admin-bell-panel {
    position: fixed; top: 3.4rem; left: 0.5rem; right: 0.5rem; width: auto;
    max-height: calc(100vh - 3.4rem - var(--ws-tabbar-h, 3.5rem) - 1rem);
    max-height: calc(100dvh - 3.4rem - var(--ws-tabbar-h, 3.5rem) - env(safe-area-inset-bottom, 0px) - 1rem);
  }
}
/* THE BELL'S WIDTH IS PAID FOR BY THE GEAR'S WORD (0147), measured, not
   reasoned: with eleven tabs the 1024px row had 31px to spare (see the 64rem
   note above) and the bell costs ~49px, so verify:admin failed one-row at 1024
   on every page. From 64rem to 80rem the gear drops "Admin" and keeps its icon —
   the same trade the phone bar already makes — and the row fits again. From
   80rem (1280px, 1048px of row) there is room for both. */
@media (min-width: 64rem) and (max-width: 79.99rem) {
  .admin-nav-gear-label { display: none; }
}
/* Settings → Notifications (0147). A two-switch table: bell and phone. */
.nt-table { width: 100%; border-collapse: collapse; margin: 0.25rem 0 0.5rem; }
.nt-table th, .nt-table td { padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--border); text-align: left; }
.nt-table thead th {
  font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.05em;
  font-size: 0.75rem; color: var(--muted); font-weight: 600;
}
.nt-table tbody th { font-weight: 500; color: var(--text); }
.nt-table td { width: 4.5rem; text-align: center; }
.nt-check { display: inline-flex; align-items: center; justify-content: center; min-width: var(--ws-tap); min-height: var(--ws-tap); cursor: pointer; }
.nt-check input { width: 1.15rem; height: 1.15rem; accent-color: var(--copper); }
.nt-h {
  font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.04em;
  font-size: 0.9rem; color: var(--steel); margin: 1rem 0 0.4rem;
}
.nt-devices { list-style: none; margin: 0; padding: 0; }
.nt-devices li { display: flex; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; padding: 0.35rem 0; border-bottom: 1px solid var(--border); }
/* WO-0009 — .btn sets display, which beats the UA [hidden]: the expired page's
   Call button must really vanish on every other closed-estimate page. */
#ev-unavailable-call[hidden] { display: none; }

/* 0155 — the acknowledgments on the client's approval page. Each one is a
   whole-row label (the box and its words are one tap target), the required
   ones marked so, the photo one plainly optional. */
.ev-acks { border: 1px solid var(--border, #E6E1D7); border-radius: var(--radius, 4px); padding: 0.9rem 1rem; margin: 0 0 1.25rem; }
.ev-acks[hidden] { display: none; }
.ev-acks-legend { font-weight: 700; padding: 0 0.35rem; }
.ev-ack { display: grid; grid-template-columns: 1.4rem 1fr; gap: 0.25rem 0.6rem; align-items: start; padding: 0.6rem 0; cursor: pointer; }
.ev-ack + .ev-ack { border-top: 1px solid var(--border, #E6E1D7); }
.ev-ack input { width: 1.25rem; height: 1.25rem; margin: 0.15rem 0 0; }
.ev-ack-text { line-height: 1.45; overflow-wrap: anywhere; }
.ev-ack-tag { grid-column: 2; font-size: 0.82rem; }
.ev-ack-copy { margin: 1rem 0; }
.ev-ack-copy[hidden] { display: none; }
.ev-ack-copy-h { font-size: 1rem; margin: 0 0 0.5rem; }
.ev-ack-copy-list { margin: 0; padding-left: 1.1rem; display: grid; gap: 0.5rem; overflow-wrap: anywhere; }
.ev-ack-copy-foot { font-size: 0.82rem; margin: 0.5rem 0 0; }
.ev-reload { margin-left: 0.4rem; }

/* 0155 — the office side: the verbal-approval dialog and the recorded acknowledgments. */
.va-form { display: grid; gap: 0.6rem; width: 100%; }
.va-form p { margin: 0; }
.va-h { font-weight: 700; }
.va-ack { display: grid; grid-template-columns: 1.4rem 1fr; gap: 0.6rem; align-items: start; overflow-wrap: anywhere; }
.va-ack input { width: 1.2rem; height: 1.2rem; margin: 0.15rem 0 0; }
.va-photo { border: 1px solid var(--border-strong); border-radius: var(--radius); padding: 0.5rem 0.75rem; display: grid; gap: 0.4rem; }
.va-photo legend { padding: 0 0.3rem; overflow-wrap: anywhere; }
.va-photo label { display: flex; align-items: center; gap: 0.5rem; min-height: 2.75rem; }
.va-note { display: grid; gap: 0.25rem; }
.va-note textarea { width: 100%; box-sizing: border-box; }
.est-acks { margin: 1rem 0; padding: 0.75rem 0.9rem; border: 1px solid var(--border); border-radius: var(--radius); }
.est-acks-h { margin: 0 0 0.5rem; font-size: 0.95rem; }
.est-acks-list { margin: 0; padding-left: 1.1rem; display: grid; gap: 0.4rem; font-size: 0.88rem; overflow-wrap: anywhere; }
.est-acks-foot { font-size: 0.8rem; margin: 0.5rem 0 0; overflow-wrap: anywhere; }
.est-acks-photo { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0.6rem 0 0; }

/* "Send a copy" of a signed contract: the confirm's optional other address. */
.copy-form { display: grid; gap: 0.6rem; width: 100%; }
.copy-to { display: grid; gap: 0.25rem; font-size: 0.9rem; }
.copy-to input { width: 100%; box-sizing: border-box; min-height: 2.75rem; }
/* The change order's own words about the contract total, shown before it's made or sent. */
.co-doc-h { margin: 0; }
.co-doc-line {
  margin: 0; padding: 0.55rem 0.75rem; border-left: 3px solid var(--copper);
  background: var(--panel-2, rgba(0,0,0,0.15)); font-weight: 600; overflow-wrap: anywhere;
}
.co-doc-stop { margin: 0; font-size: 0.85rem; }

/* WO-0024 — "Show transactions" on the invoice sheet: every invoice on the job
   and every payment, refund and reversal against each. */
.inv-tx-sum { cursor: pointer; min-height: 2.75rem; display: flex; align-items: center; gap: 0.4rem; font-weight: 600; color: var(--copper); }
.inv-tx-body { display: grid; gap: 0.6rem; padding-top: 0.4rem; min-width: 0; }
.tx-left, .tx-state, .tx-money, .tx-inv-head, .tx-none, .tx-pending { margin: 0; }
.tx-money { display: flex; flex-wrap: wrap; gap: 0.25rem 0.75rem; font-size: 0.9rem; }
.tx-invoices { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.6rem; }
.tx-inv { display: grid; gap: 0.3rem; padding: 0.6rem 0.75rem; border: 1px solid var(--border); border-radius: var(--radius); min-width: 0; }
.tx-inv.is-current { border-color: var(--copper); }
.tx-inv-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.6rem; }
.tx-this { font-size: 0.78rem; color: var(--copper); font-weight: 600; }
.tx-state { font-size: 0.9rem; }
.tx-rows { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.25rem; }
.tx-row { display: grid; grid-template-columns: auto 1fr auto; gap: 0.6rem; align-items: baseline; font-size: 0.9rem; min-width: 0; }
.tx-when { color: var(--muted); white-space: nowrap; }
.tx-what { min-width: 0; overflow-wrap: anywhere; }
.tx-amt { font-variant-numeric: tabular-nums; white-space: nowrap; font-weight: 600; }
.tx-row.is-out .tx-amt { color: var(--muted); }

/* ── WO-0019 — THE JOBS LIST (admin-pipeline.js) ─────────────────────────────
   A spreadsheet from 46rem: Client · What they want · Where it stands, one job
   a row, each row opening in place to its comments. Below 46rem the same
   markup becomes CARDS — a three-column table does not read at 390px — and the
   column header goes, because each card says what it is. The whole section is
   a <details>: on a phone it starts as one line, so the calendar below it is
   still on the first screen. */
.pipe > .pipe-head { list-style: none; cursor: pointer; min-height: var(--ws-tap); display: flex; align-items: center; }
.pipe > .pipe-head::-webkit-details-marker { display: none; }
.pipe > .pipe-head .dash-h { margin: 0; justify-content: flex-start; }
.pipe > .pipe-head .dash-h::after { content: "▸"; color: var(--muted); margin-left: auto; }
.pipe[open] > .pipe-head .dash-h::after { content: "▾"; }
.pipe-count { font-family: var(--font-body); text-transform: none; letter-spacing: 0; font-weight: 400; }
.pipe-bar { display: flex; justify-content: flex-end; margin: 0 0 var(--ws-gap-xs); }
.pipe-bar:empty { display: none; }
.pipe-closed-btn { min-height: var(--ws-tap); }
.pipe-grid { border-top: 1px solid var(--border); }
.pipe-cols { display: none; }
.pipe-row { border-bottom: 1px solid var(--border); }
.pipe-row.is-closed { opacity: 0.7; }
.pipe-line {
  list-style: none; cursor: pointer; display: grid; gap: 0.15rem 0.75rem;
  grid-template-columns: minmax(0, 1fr) auto; padding: 0.6rem 0; min-height: var(--ws-tap);
}
.pipe-line::-webkit-details-marker { display: none; }
.pipe-who { display: flex; flex-direction: column; min-width: 0; }
.pipe-name { overflow-wrap: anywhere; }
.pipe-place, .pipe-since, .pipe-note { font-size: 0.8rem; }
.pipe-want { grid-column: 1 / -1; grid-row: 2; font-size: 0.9rem; color: var(--steel); overflow-wrap: anywhere; }
.pipe-status { grid-column: 2; grid-row: 1; display: flex; flex-direction: column; align-items: flex-end; text-align: right; gap: 0.1rem; }
.pipe-pill {
  font-size: 0.75rem; font-weight: 600; padding: 0.15rem 0.45rem; border-radius: var(--radius);
  border: 1px solid var(--border-strong); color: var(--text); white-space: nowrap;
}
.pipe-row--estimate .pipe-pill { border-color: var(--steel); }
.pipe-row--contract .pipe-pill { border-color: var(--copper-bright); }
.pipe-row--work .pipe-pill { border-color: var(--copper); color: var(--copper-bright); }
.pipe-row--closing .pipe-pill { background: var(--copper); border-color: var(--copper); color: var(--bg); }
.pipe-row--closed .pipe-pill { color: var(--muted); }
.pipe-body { padding: 0 0 var(--ws-gap); }
.pipe-links { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem var(--ws-gap); margin: 0 0 var(--ws-gap-sm); font-size: 0.85rem; }
.pipe-link { color: var(--copper); font-weight: 600; text-decoration: none; min-height: var(--ws-tap); display: inline-flex; align-items: center; }
.pipe-from { font-size: 0.75rem; }
.pipe-empty { padding: var(--ws-gap-sm) 0; }
@media (min-width: 46rem) {
  .pipe-cols, .pipe-line { grid-template-columns: minmax(10rem, 1.1fr) minmax(0, 1.6fr) minmax(11rem, 1fr); }
  .pipe-cols {
    display: grid; gap: 0 0.75rem; padding: 0.35rem 0; border-bottom: 1px solid var(--border);
    font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted);
  }
  .pipe-cols span:last-child { text-align: right; }
  .pipe-want { grid-column: 2; grid-row: 1; }
  .pipe-status { grid-column: 3; }
}
@media (min-width: 64rem) {
  .forge > #pipe-slot { grid-column: 1 / -1; }
}
/* 2026-10-11 — the open invoices claim more than the job owes (overlap). */
.tx-warn { margin: 0; font-size: 0.9rem; padding: 0.5rem 0.65rem; border-radius: var(--radius); border: 1px solid var(--border); background: var(--panel); }

/* ── WO-0008 — JOB DETAILS (admin-job-details.js) ───────────────────────────
   One row per line: the name with its checkbox at the end, the notes and the
   materials under it. Ticked rows dim their text but stay readable — a done
   line is still a line someone may need to check. */
.jdl-sub { margin: 0 0 0.15rem; font-weight: 600; }
.jdl-office { margin: 0 0 var(--ws-gap-sm); font-size: 0.8rem; }
.jdl-summary { margin: 0 0 var(--ws-gap-sm); font-size: 0.9rem; color: var(--steel); }
.jdl-summary:empty { display: none; }
.jdl-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--border); }
.jdl-row { padding: 0.6rem 0; border-bottom: 1px solid var(--border); }
.jdl-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--ws-gap-sm); }
.jdl-name { font-weight: 600; overflow-wrap: anywhere; }
.jdl-origin { font-weight: 400; font-size: 0.8rem; }
/* WO-0008 r4 — each line is a bullet that opens. The whole title is the
   button, so the tap target is the row's full width and at least --ws-tap tall. */
.jdl-toggle {
  display: flex; align-items: flex-start; gap: 0.5rem; flex: 1 1 auto; min-width: 0;
  min-height: var(--ws-tap); margin: -0.35rem 0; padding: 0.35rem 0.25rem 0.35rem 0;
  background: none; border: 0; border-radius: 6px; text-align: left; cursor: pointer;
  font: inherit; color: inherit; text-transform: none; letter-spacing: 0;
}
.jdl-toggle:focus-visible { outline: 2px solid var(--copper); outline-offset: 2px; }
.jdl-arrow {
  flex: none; width: 1.1rem; height: 1.1rem; margin-top: 0.15rem; position: relative;
}
.jdl-arrow::before {
  content: ""; position: absolute; left: 0.3rem; top: 0.25rem;
  border-style: solid; border-width: 0.3rem 0 0.3rem 0.42rem;
  border-color: transparent transparent transparent var(--steel);
  transition: transform 0.15s ease; transform-origin: 30% 50%;
}
.jdl-toggle[aria-expanded="true"] .jdl-arrow::before { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .jdl-arrow::before { transition: none; } }
.jdl-title { display: block; min-width: 0; }
.jdl-has { display: block; font-size: 0.78rem; font-weight: 400; margin-top: 0.1rem; }
.jdl-more { padding: 0.35rem 0 0.1rem 1.6rem; }
.jdl-more[hidden] { display: none; }
.jdl-sec + .jdl-sec { margin-top: 0.55rem; }
.jdl-sec > .jdl-k { display: block; margin: 0; }
.jdl-photos { list-style: none; margin: 0.3rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem; }
.jdl-photo { display: block; min-width: var(--ws-tap); min-height: var(--ws-tap); border-radius: 6px; overflow: hidden; border: 1px solid var(--border); }
.jdl-photo img { display: block; width: 6.5rem; height: 6.5rem; object-fit: cover; }
.jdl-tick {
  display: inline-flex; align-items: center; gap: 0.4rem; flex: none;
  min-height: var(--ws-tap); min-width: var(--ws-tap); cursor: pointer;
  /* The global label rule is a form field's caption: margin above, display caps. */
  margin: -0.6rem 0 -0.6rem; font-family: var(--font-body); font-weight: 400;
  text-transform: none; letter-spacing: 0; font-size: 0.85rem; color: var(--steel);
}
.jdl-box { width: 1.35rem; height: 1.35rem; accent-color: var(--copper); }
.jdl-box:disabled + .jdl-tick-word { color: var(--muted); }
.jdl-notes { margin: 0.15rem 0 0; font-size: 0.9rem; white-space: pre-line; overflow-wrap: anywhere; }
.jdl-mat { margin: 0.15rem 0 0; font-size: 0.85rem; white-space: pre-line; overflow-wrap: anywhere; }
.jdl-k { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin-right: 0.35rem; }
.jdl-none { margin: 0.2rem 0 0; font-size: 0.85rem; }
.jdl-state { margin: 0.2rem 0 0; font-size: 0.78rem; }
.jdl-state:empty { display: none; }
.jdl-row.is-done .jdl-name, .jdl-row.is-done .jdl-notes, .jdl-row.is-done .jdl-mat { color: var(--steel); }
.jdl-row.is-done .jdl-name { text-decoration: line-through; text-decoration-color: var(--muted); }

/* --- WO-0071: an archived client on a list -------------------------------
   The badge a row wears when its client is archived (shown by "Show archived",
   or kept in view because it still holds money), the toggle's own line, and
   the banner over an archived client's record. Quiet, never alarming: an
   archive is filing, not a fault. */
.badge-archived { color: var(--muted); border-color: var(--border-strong); border-style: dashed; }
.archived-row { margin: 0.25rem 0 0.5rem; }
.archived-toggle { min-height: 44px; }
.archived-notice { display: inline-block; margin-left: 0.5rem; font-size: 0.85rem; }
.archived-banner {
  margin: 0 0 0.75rem; padding: 0.5rem 0.75rem;
  border: 1px dashed var(--border-strong); border-radius: 6px;
  color: var(--text); font-size: 0.9rem; overflow-wrap: anywhere;
}

/* --- My visits (WO-0078) ----------------------------------------------------
   The estimator's home, built for a phone in a driveway: one card per visit,
   every control a full 2.75rem target, nothing that needs a hover. */
.my-visits .mv-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.mv-card { display: grid; gap: 0.45rem; }
.mv-card > * { margin: 0; }
.mv-time { font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.03em; font-size: 0.9rem; color: var(--copper); }
.mv-name { font-size: 1.25rem; }
.mv-address { color: var(--text); }
.mv-label { font-weight: 600; color: var(--steel); margin-right: 0.25rem; }
.mv-desc { color: var(--muted); }
.mv-contact, .mv-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.mv-contact .btn, .mv-actions .btn, .mv-photo-add {
  min-height: 2.75rem; min-width: 2.75rem; display: inline-flex; align-items: center; justify-content: center;
}
.mv-photos-wrap { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.mv-photos { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem; }
.mv-photo {
  display: block; width: 4.5rem; height: 4.5rem; padding: 0; margin: 0; min-height: 0;
  border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: var(--panel-2); cursor: zoom-in;
}
.mv-photo img { display: block; width: 100%; height: 100%; object-fit: cover; }
.mv-photo-add.is-busy { opacity: 0.6; pointer-events: none; }
.mv-past { margin-top: 1.5rem; }
.mv-past-sum { cursor: pointer; min-height: 2.75rem; display: flex; align-items: center; font-weight: 600; }
.mv-past .mv-list { margin-top: 0.75rem; }

/* --- Estimators, seen from the office, and the estimator's banners (WO-0078) --- */
.badge.est-ready-flag { color: var(--copper); border-color: var(--copper); }
.badge.est-by { color: var(--steel); }
.est-ready-banner, .est-readonly-why {
  margin: 0.75rem 0; padding: 0.7rem 0.9rem; border-radius: var(--radius);
  border: 1px solid var(--border-strong); background: var(--panel-2);
}
.est-ready-banner { border-left: 3px solid var(--copper); }
.est-readonly-view { margin-top: 1rem; }
.user-role-label { display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.9rem; color: var(--steel); }
.user-role-label select { min-height: 2.75rem; }

/* WO-0078 — the role filter's [hidden] must win. .admin-nav-link and
   .more-item are display:inline-flex/flex, which beats the UA's
   [hidden]{display:none} — the trap .admin-nav[hidden] documents. Until the
   estimator, every tab was shown to every role and nothing tested this; the
   static markup now carries My visits hidden for the office. */
.admin-nav-link[hidden], .more-item[hidden], .more-grid[hidden], .more-group[hidden], .admin-nav-gear-h[hidden] { display: none; }

/* --- The estimate builder at 390 (WO-0078, item 4 — Hector's findings) -------
   For every role, not only the estimator. Measured before, at 390x844 on an
   open line: the grip was 27x889 with touch-action:none (a finger anywhere
   down the left edge of an open line dragged instead of scrolling); the
   library gear 21x21; the tax checkbox's label 100x25; the materials summary
   304x27; the nav gear 36x44. */

/* THE GRIP IS A HANDLE, NOT A RAIL. 0086 made it the row's full height so the
   whole left edge dragged; on a phone an open line is ~890px tall and that edge
   is where a thumb scrolls. It is now one 2.75rem target at the row's top
   (still touch-action:none — a finger ON it drags, anywhere else scrolls); the
   full-height divider it drew goes with it, and the row's own copper left
   border still marks the line. */
.admin .el-drag { bottom: auto; height: 2.75rem; border-right: 0; border-radius: var(--radius); }
/* Padding beside the grip: the row's content no longer starts flush with it. */
.est-line, .est-line--collapsed { padding-left: calc(2.1rem + 0.5rem); }
@media (max-width: 34rem) {
  .est-line, .est-line--collapsed { padding-left: calc(1.7rem + 0.5rem); }
}

/* Tap targets of 2.75rem. */
.est-lib-gear { min-height: 2.75rem; min-width: 2.75rem; gap: 0.35rem; padding: 0.2rem 0.5rem; justify-content: center; }
.est-lib-gear-label { font-size: 0.85rem; }
.est-tax-toggle { min-height: 2.75rem; }
.est-tax-toggle input { width: 1.25rem; height: 1.25rem; }
.el-materials-sum { min-height: 2.75rem; display: flex; align-items: center; }
/* The nav gear at 390 only: from 46rem up it shares the one-row nav, and 8px
   more wrapped the row at 1024 (verify:admin "the nav is ONE row"). */
@media (max-width: 45.99rem) { .admin .admin-nav-gear-btn { min-width: 2.75rem; justify-content: center; } }

/* Tap-to-explain (replaces hover-only title help). */
.el-help { display: inline-block; position: relative; margin-left: 0.25rem; }
.el-help-btn {
  list-style: none; cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
  min-width: 2.75rem; min-height: 2.75rem; color: var(--muted); font-weight: 700;
}
.el-help-btn::-webkit-details-marker { display: none; }
.el-help-btn:hover { color: var(--text); }
.el-help-text { display: block; font-size: 0.85rem; color: var(--muted); max-width: 22rem; padding: 0 0 0.4rem; }

/* A STICKY SAVE BAR ON PHONES: Save and the one primary beside it (Send, or
   the estimator's Ready for review) stay in reach under a long quote instead of
   waiting at the foot of it, just above the bottom tab bar. The previews stay
   in the page below — a two-row bar would cover a fifth of the screen.
   ⚠️ .est-actions is display:contents on a phone so the sticky group's
   constraint is the whole builder, not the short row it sits in; from 46rem up
   the group is the one that is display:contents, and the row is unchanged.
   #estimates only: the contract editor shares .est-actions and isn't touched. */
#estimates .est-actions-sticky { display: contents; }
#estimates .est-actions #est-msg { order: 1; }
@media (max-width: 45.99rem) {
  #estimates .est-actions { display: contents; }
  #estimates .est-actions > .btn { margin: 0.5rem 0.6rem 0 0; }
  #estimates .est-actions-sticky {
    display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem;
    position: sticky; bottom: calc(var(--ws-tabbar-h) + env(safe-area-inset-bottom, 0px)); z-index: 5;
    background: var(--bg); border-top: 1px solid var(--border);
    margin: 0.5rem -1rem 0; padding: 0.5rem 1rem;
  }
  #estimates .est-actions-sticky .btn { min-height: 2.75rem; font-size: 0.9rem; padding: 0.5rem 1rem; }
}

/* SEND FEEDBACK (WO-0092). The link is quiet text — not a button to look at —
   but a whole 44px target to press: a button, because it opens a pop-up rather
   than going anywhere. */
.feedback-link {
  background: none; border: 0; padding: 0 0.25rem; min-height: var(--ws-tap, 44px); min-width: 44px;
  font: inherit; font-size: 0.85rem; color: var(--muted);
  text-decoration: underline; text-underline-offset: 0.2em; cursor: pointer;
}
.feedback-link:hover, .feedback-link:focus-visible { color: var(--text); }
.feedback-foot { margin: 1.5rem 0 0; text-align: right; }
/* The phone reaches Feedback through the More sheet. The gear's own stays out
   of the phone's gear, whose open menu already runs wider than 390. */
@media (max-width: 45.99rem) { .admin-nav-gear-items .feedback-link { display: none; } }
.more-feedback { text-align: center; }
.fb-lede { margin: 0 0 0.75rem; }
.feedback-modal textarea { width: 100%; box-sizing: border-box; min-height: 8rem; resize: vertical; }
.fb-count { margin: 0.3rem 0 0; font-size: 0.8rem; text-align: right; }
.fb-count.is-over { color: #F2B8A0; }
.feedback-modal .ws-modal-foot .btn { min-height: 44px; }
