/* MVI field tools — the shared vocabulary.
 *
 * WHY THIS FILE EXISTS, and it is not "tidiness". Before 7 Sep 2026 there was no .css file
 * in this repo at all: 88 KB of inline <style>, spread over sixteen pages, carrying 114
 * distinct hex colours, 20 distinct font sizes (including 10.5, 11.5, 12.5, 13.5 and 14.5
 * as separate steps) and 12 border-radius values. That is not sixteen designs. It is one
 * design typed out sixteen times by the same person on different days, each time picking a
 * number that looked right on its own.
 *
 * THE PROOF IT IS ONE DESIGN: nine pages already declared an IDENTICAL :root block --
 * --ink --sub --line --bg --panel --accent --accent-d --danger --good, character for
 * character, nine copies. A design system already existed in fact. It just had no home and
 * no way to grow: --warn and --tap made it into three of the nine and stopped there.
 *
 * HOW THIS ROLLS OUT, and why step one cannot break anything:
 *
 *   A. This file ships and every page links it. It declares TOKENS and OPT-IN CLASSES,
 *      every one of them prefixed `mvi-`. There is not a single element selector and not
 *      one unprefixed class, so on a page that has not been converted there is nothing
 *      for it to affect. The nine duplicate :root blocks stay where they are for now --
 *      they are byte-identical to the ones here, and a page's own <style> comes after
 *      this <link> and would win anyway.
 *   B. index.html is rebuilt on this vocabulary. It is the first consumer and the proof
 *      the vocabulary is sufficient.
 *   C. Pages convert one at a time, busiest first, deleting local rules as these replace
 *      them. Each page stands alone; any one of them reverts on its own.
 *
 * TWO SURFACES, ONE VOCABULARY. The tools are light (#f7f7f8) and the launcher shell is
 * dark. Rather than two sets of components, `.mvi-shell` REBINDS the same token names to
 * the dark values, so every component below works on either surface with no variant of
 * its own. Add a component once; it is correct on both.
 *
 * THE DARK GROUND WAS NAVY (#0f172a) UNTIL 15 Sep 2026 and is now MultiView green. The
 * split itself is still deliberate and is now a decision rather than an inheritance: the
 * tools stay light because they show photographs whose exposure is being judged, they are
 * long forms, and light-on-dark is the worse combination in bright sun. Green chrome,
 * light instruments.
 *
 * WHAT NO TEST CAN CATCH HERE. The tamper sweep asserts logic, and there is nothing to
 * assert about a wrong margin. The guard that fits a stylesheet is a screenshot baseline
 * -- test/inspection/shot.js already drives a real page in Playwright -- and it is worth
 * building alongside step C rather than before it.
 */

:root {
  /* ── the nine that nine pages already agreed on ─────────────────────────────
     Copied verbatim. Changing any of these values is a redesign, not a refactor. */
  --ink:        #1f2937;   /* body text */
  --sub:        #6b7280;   /* secondary text */
  --line:       #d1d5db;   /* borders */
  --bg:         #f7f7f8;   /* page ground */
  --panel:      #ffffff;   /* cards, sheets, anything raised off the ground */
  /* MULTIVIEW GREEN, 15 Sep 2026 — was #2563eb / #1d4ed8.
     Not a new colour: these are the two `.mvi-portal` had been declaring for itself, so
     this promotes them from a portal override to the system accent and that block goes
     away. Dan's rule was "the color has to be standard, no new versions or shades", and
     the only way to obey it while making the product one colour was to use a value the
     product already had.

     WHY NOT --brand #769d46, which is the obvious candidate: 3.14:1 on white, which fails
     WCAG AA for body text. This is 9.97:1, where the blue it replaces was 5.17:1. A
     tagline colour and a link colour are different jobs -- the comment this pair used to
     sit under said so from instinct, and the measurement agrees with it.

     WHAT THIS WAS FOR. book.html opted into .mvi-portal and was MultiView green; nothing
     else ever did. A client went green website -> green booking page -> BLUE
     agreement-signed -> NAVY-AND-BLUE login, to read their own report. */
  /* ONE DEFINITION. The accent on a light page and the GROUND of the dark shell are the
     same green on purpose — it is the brand in two roles, not two colours — so it is
     declared once here and pointed at twice. Writing the hex in both places is the
     "second thing to update and second thing to forget" that .mvi-portal's duplicate
     --accent was deleted for on this same day. */
  --forest:     #1d4b33;
  --accent:     var(--forest);
  --accent-d:   #163a28;   /* the pressed/hover half of accent */
  --danger:     #dc2626;
  --good:       #16a34a;

  /* Reached three of the nine and stopped. Declared for all of them here.
     --tap is the minimum touch target: 56px, set for a gloved thumb on an iPad, and it
     is the reason nothing in this file has a control shorter than that. */
  --warn:       #b45309;
  --tap:        56px;

  /* ── the neutrals that were hand-typed instead ───────────────────────────── */
  --ink-strong: #111827;   /* headings, and a number the reader has to find */
  --sub-soft:   #9ca3af;   /* placeholder, disabled, the third rank of text */
  --line-soft:  #e5e7eb;   /* a divider INSIDE a container, not around it */
  --wash:       #f3f4f6;   /* a resting fill: hovered row, inactive tab, code */

  /* ── the four semantic families, each a fill, an edge and an ink ───────────
     Every one of these was already in use as a trio -- .bad is literally
     {background:#fef2f2;border-color:#fecaca;color:#991b1b} in inspection.html -- typed
     out in seven to nine files each. Semantic colour is separate from --accent on
     purpose: an accent says "this is ours", a semantic says "this is the state of your
     house", and the two must never be readable as the same thing. */
  --danger-bg:  #fef2f2;  --danger-line: #fecaca;  --danger-ink: #991b1b;
  --warn-bg:    #fffbeb;  --warn-line:   #fde68a;  --warn-ink:   #92400e;
  /* ONE GREEN, 11 Sep 2026. --good is green-600 but this trio was EMERALD, while the
     pages had been hand-typing green-50/200/800 all along (#f0fdf4, #bbf7d0, #166534,
     18 uses between them). Two greens in one system is the thing this file exists to
     end, and the pages were the ones getting it right. */
  --good-bg:    #f0fdf4;  --good-line:   #bbf7d0;  --good-ink:   #166534;
  --info-bg:    #eef4ff;  --info-line:   #c7d7fe;  --info-ink:   #1e3a8a;

  /* ── the dark surface, named ────────────────────────────────────────────────
     .mvi-shell rebinds --bg/--panel/--line/--sub/--ink to these, and six pages that
     never opted into the class were hand-typing the same five values anyway: #0f172a
     x16, #334155 x12, #e2e8f0 x12, #94a3b8 x8, #1e293b x5. Named here so there is one
     definition of the dark ground, and .mvi-shell points at it rather than repeating it. */
  --slate-900:  #0f172a;   /* the dark ground */
  --slate-800:  #1e293b;   /* a panel raised off it */
  --slate-700:  #334155;   /* a border on it */
  --slate-400:  #94a3b8;   /* secondary text on it */
  --slate-200:  #e2e8f0;   /* body text on it */

  /* ── the company colour ─────────────────────────────────────────────────────
     MultiView's own green, on the tagline under the logo. NOT a palette slot and not a
     semantic: it says whose report this is. Exempt from every consolidation rule for
     exactly that reason -- see template/adopt_mvi_css.py. */
  --brand:      #769d46;

  /* ── the two colours that only work on the dark ground ──────────────────────
     A semantic on a dark ground cannot be the light one -- --danger #dc2626 on the shell
     ground is nearly unreadable -- so these are the SAME families at the strength that
     carries, and .mvi-shell points at them rather than repeating them.

     THERE WERE THREE. --accent-soft #60a5fa was "the accent, on the dark ground" back
     when that ground was navy, and it was the last blue left in this file. On 15 Sep the
     six dark-surface pages (404, the four sign-in screens, the launcher) all put on
     .mvi-shell, where the accent is WHITE on forest -- and the token lost its last
     consumer. A declared colour with no consumer is not inert: it is the next person's
     shortcut back to a palette that no longer exists, and it reads as sanctioned because
     it is sitting in :root. So it is deleted rather than kept "in case". Measured before
     deleting: zero var(--accent-soft) references outside comments, tree-wide.
     design_system.js now fails on ANY unreferenced colour token in :root, so the next
     one cannot sit here quietly the way this one did. */
  --danger-soft: #fca5a5;  /* red, on the dark ground */
  --good-soft:   #4ade80;  /* green, on the dark ground */

  /* ── type ────────────────────────────────────────────────────────────────
     Twenty sizes became eight. The scale is derived from what the pages actually use,
     not chosen from a chart: 13px is the workhorse (69 uses), 12px next (50). The half
     steps -- 10.5, 11.5, 12.5, 13.5, 14.5 -- each round to a neighbour, and none of
     them was ever a decision. */
  --t-2xs:  11px;   /* uppercase labels and eyebrows */
  --t-xs:   12px;   /* meta, captions, chips */
  --t-sm:   13px;   /* the workhorse: descriptions, hints, secondary rows */
  --t-base: 14px;   /* body copy and control text */
  --t-md:   15px;   /* a row's own title */
  --t-lg:   17px;   /* a card's title */
  --t-xl:   20px;   /* a screen heading */
  --t-2xl:  22px;   /* the page title, once */

  /* Two more, added 11 Sep 2026, each because something real needs it rather than to
     round out a chart. */
  --t-form: 16px;   /* ANY text input. Below 16px iOS zooms the page on focus, which on
                       an iPad in a house is a lurch mid-sentence. Not a design choice. */
  --t-num:  34px;   /* a display numeral -- a reading, a count, a temperature. The only
                       thing on a screen meant to be read from arm's length. */

  --face: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* ── radius and space ────────────────────────────────────────────────────
     Twelve radii became four. Three of the twelve (5, 6, 7px) were the same intention
     three times. */
  /* MULTIPLES OF FOUR, 11 Sep 2026. Twelve radii became four on 7 Sep, but on 6/10/14 --
     numbers picked from what happened to be in the files. Across the pages the real
     spread was 8px x42, 7px x16, 11px x9, 9px x5, 5px x6, 3px x4: nobody was choosing,
     everybody was approximating. A 4/8/12 ladder is what iOS, Material and Tailwind all
     use, it is what every other toolkit a device may render this next to uses, and every
     one of those strays is within two pixels of a rung. */
  --r-sm:   4px;
  --r-md:   8px;
  --r-lg:   12px;
  --r-pill: 999px;

  --s-1: 4px;  --s-2: 8px;  --s-3: 12px;  --s-4: 16px;  --s-5: 24px;  --s-6: 32px;
}

/* ── the dark surface ───────────────────────────────────────────────────────
 * The launcher shell. Same token NAMES, different values, so every component below is
 * correct on this surface without knowing it exists. Put `mvi-shell` on the element that
 * owns the dark ground -- normally <body> on index.html.
 *
 * THE SLATE RAMP IS NO LONGER THIS SURFACE'S GROUND. Until 15 Sep these were
 * index.html's own colours -- #0f172a / #1e293b / #334155 / #e2e8f0 / #94a3b8 / #60a5fa,
 * the Tailwind slate ramp the shell was written against. The ground and the raised
 * surfaces are now --forest and tints of white; --slate-200 stays as the body ink and the
 * rest of the ramp is still declared because six pages that never wore this class are
 * still hand-typing it. Do not read the ramp's existence as a claim about this block. */
/* ── the modal a shared picker needs ──────────────────────────────────────────
   MVI.openPendingJobsPicker injects only its own pj-prefixed rules and a max-width; it
   ASSUMES the host page already gives the overlay and the modal a base look. appliance.html
   had one from its equipment wizard, intake.html did not -- and the picker rendered
   see-through on 31 Aug 2026 until intake copied appliance's rule verbatim. The launcher
   would have been the third copy, so it is here instead, and the next page to want a picker
   gets it by adding two classes.

   Anchored on .mvi-* like everything else in this file, so a page opts in and no page that
   has its own #overlay/#modal rules is touched by it. */
.mvi-overlay {
  position: fixed; inset: 0; background: rgba(17, 24, 39, .5); display: none;
  align-items: flex-start; justify-content: center; padding: 30px 14px;
  overflow: auto; z-index: 50;
}
.mvi-overlay.show { display: flex; }
.mvi-modal {
  background: var(--panel); border-radius: var(--r-md); padding: 22px 24px;
  width: 100%; max-width: 560px; box-shadow: 0 10px 40px rgba(0, 0, 0, .25);
  color: var(--ink);
}
.mvi-modal h3 { margin: 0 0 14px; }

/* ── the "Text client" sheet ───────────────────────────────────────────────────
   Four rows a man presses one-handed, in a driveway, on a phone. Everything here is
   sized off --tap for that reason: a mis-tap does not show a wrong number, it sends a
   real text to a real client, and there is no undo on the far end.

   Anchored on .mvi-tc* like every other component in this file, so a page opts in by
   using the classes and no page that does not is touched. */
.mvi-tcModal { max-width: 460px; }
.mvi-tcWhere { color: var(--sub); font-size: var(--t-sm); margin: -8px 0 16px; }
.mvi-tcRow {
  border: 1px solid var(--line); border-radius: var(--r-md);
  padding: 12px 14px; margin-bottom: 10px; background: var(--panel);
}
.mvi-tcRow.is-sent { background: var(--good-bg); border-color: var(--good); }
/* Muted, not disabled: the other finishing message has gone, which is a thing to know
   and not a thing to forbid — see TC_FINISHERS in mvi-shared.js. */
.mvi-tcRow.is-muted { opacity: .62; }
.mvi-tcGo, .mvi-tcLabel {
  display: block; width: 100%; text-align: left; font: inherit;
  font-size: var(--t-md); font-weight: 600; color: var(--ink);
  background: none; border: 0; padding: 0; min-height: var(--tap); cursor: pointer;
}
.mvi-tcLabel { cursor: default; min-height: 0; }
.mvi-tcNote { color: var(--sub); font-size: var(--t-sm); margin-top: 2px; }
.mvi-tcMins { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 8px; }
.mvi-tcMin {
  font: inherit; font-size: var(--t-md); font-weight: 600; color: var(--ink);
  background: var(--wash); border: 1px solid var(--line); border-radius: var(--r-sm);
  min-width: var(--tap); min-height: var(--tap); cursor: pointer;
}
/* --wash, NOT --accent-soft. That token is "the accent, on the dark ground" from the navy
   world, and a light BLUE hover on a green card was the last live blue in this stylesheet
   after 15 Sep. --wash is the token for exactly this -- "a resting fill: hovered row,
   inactive tab" -- and it rebinds per surface, so the hover is right on both. */
.mvi-tcMin:hover { background: var(--wash); border-color: var(--accent); }
.mvi-tcUnit { color: var(--sub); font-size: var(--t-sm); }
.mvi-tcNone { color: var(--sub); font-size: var(--t-sm); }
/* THE WHOLE SHEET GOES INERT WHILE ONE SEND IS IN FLIGHT, because the second tap of a
   fumble lands on the row below the one that was aimed at, and the endpoint cannot
   catch a true simultaneous double — it reads a booking's history that it only writes
   afterwards. */
.mvi-tcModal.is-busy .mvi-tcGo,
.mvi-tcModal.is-busy .mvi-tcMin { pointer-events: none; opacity: .55; }
.mvi-tcOut {
  margin-top: 14px; padding: 10px 12px; border-radius: var(--r-sm);
  font-size: var(--t-sm); background: var(--wash); color: var(--ink);
}
.mvi-tcOut.mvi-tc-ok { background: var(--good-bg); color: var(--good); }
.mvi-tcOut.mvi-tc-bad { background: var(--danger-bg); color: var(--danger); }
.mvi-tcOut.mvi-tc-info { background: var(--info-bg); color: var(--ink); }

/* ── THE DARK GROUND WENT GREEN, 15 Sep 2026 ──────────────────────────────────
   index.html is the only page that wears this class, and it was the only navy surface in
   a product whose website, booking page and tagline are all MultiView green. Dan: *"The
   eventual goal is to have consistency across everything."*

   NOT ONE NEW COLOUR, which was his other instruction. The ground is --forest, the same
   green --accent points at. Everything raised off it is WHITE AT AN ALPHA -- the idiom
   this block already used for its semantic families, and the reason the comment below
   them says "rgba rather than a flat hex so the panel underneath still reads through".
   A tint of white is not a new shade; it is the ground with less of itself.

   THE ONE THAT MOVED FOR A MEASURED REASON: --sub was --slate-400, which is 5.6:1 on
   navy and only 3.27:1 on green -- under AA for body text, on the tool descriptions,
   which are the most-read text on the screen. White at .66 is 4.72:1 on a card and 5.62
   on the ground, and still a clear step down from --ink.

   AND THE ACCENT DOES NOT ALSO GO GREEN. --brand on this ground is 3.17:1 and --good is
   3.02:1: both fail, and both would be the same green as the PASS state. The ground is
   already the brand statement; spend the colour twice and there is nothing left to say
   "this is fine" with. So the accent here is white, the primary button is white with
   forest text (9.97:1, overridden below because .mvi-btn--primary hard-codes white
   text), and green on this screen means state and only state. */
.mvi-shell {
  --ink:        var(--slate-200);
  --ink-strong: #ffffff;
  --sub:        rgba(255, 255, 255, .66);
  --sub-soft:   rgba(255, 255, 255, .42);
  --line:       rgba(255, 255, 255, .14);
  --line-soft:  rgba(255, 255, 255, .08);
  --bg:         var(--forest);
  /* OPAQUE, AND IT HAS TO BE. --panel is not only a card on the ground: the pending-jobs
     picker uses it as the MODAL's background, and a 6%-alpha modal is a job list with the
     launcher showing through it. `launcher_job.js` asserts exactly that and went red on
     the first version of this block, which is the check doing its job.

     color-mix rather than a hand-typed #2b563f: the value is the same green lightened by
     the same 6% white as every other surface here, computed by the browser rather than
     picked by me -- which is what "no new versions or shades" has to mean in a file where
     a raised surface still needs to be distinguishable from its ground. */
  --panel:      color-mix(in srgb, #ffffff 6%, var(--forest));
  --wash:       rgba(255, 255, 255, .10);
  --accent:     var(--ink-strong);
  --accent-d:   var(--slate-200);

  /* On a dark ground a pale tint is a glare, so the semantic families invert: the fill
     becomes a low-alpha wash of the same hue and the ink becomes the light end of the
     ramp. rgba rather than a flat hex so the panel underneath still reads through and one
     value works on both --bg and --panel. */
  --danger-bg:  rgba(220,38,38,.14);  --danger-line: rgba(220,38,38,.42);  --danger-ink: var(--danger-soft);
  --warn-bg:    rgba(217,119,6,.16);  --warn-line:   rgba(217,119,6,.45);  --warn-ink:   #fbbf24;
  /* THE ONE FAMILY THE GREEN GROUND BREAKS. A green wash on a green ground is not a
     fill, it is a slightly different green -- so `good` keeps its EDGE and its INK, both
     of which carry, and its fill becomes a neutral lift off the ground. The signal moves
     from hue to ink and shape, which is the same reasoning that put a tick inside the
     readiness dot rather than colouring it and hoping. */
  --good-bg:    rgba(255,255,255,.07);  --good-line:   var(--good-soft);  --good-ink:   var(--good-soft);
  --info-bg:    rgba(96,165,250,.13); --info-line:   rgba(96,165,250,.4);  --info-ink:   #bfdbfe;

  background: var(--bg);
  color: var(--ink);
}

/* ═══ components ═══════════════════════════════════════════════════════════
 * Every selector below is prefixed `mvi-`. Nothing here is an element selector and
 * nothing is unprefixed, which is what makes shipping this file to an unconverted page a
 * no-op. Keep it that way: the day one of these becomes `button { }` is the day linking
 * this file stops being safe.
 */

.mvi-app {
  margin: 0;
  font-family: var(--face);
  background: var(--bg);
  color: var(--ink);
  -webkit-tap-highlight-color: transparent;
}
.mvi-app *, .mvi-app *::before, .mvi-app *::after { box-sizing: border-box; }

/* Focus, once, for everything. A visible focus ring is not optional -- the tools are
   driven by a Bluetooth keyboard often enough at a desk, and Playwright drives them by
   keyboard every night. */
.mvi-app :focus-visible,
.mvi-btn:focus-visible,
.mvi-row:focus-visible,
.mvi-field:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── text ─────────────────────────────────────────────────────────────────── */
.mvi-h1     { font-size: var(--t-2xl); font-weight: 600; line-height: 1.2; margin: 0; color: var(--ink-strong); }
.mvi-h2     { font-size: var(--t-xl);  font-weight: 600; line-height: 1.25; margin: 0; color: var(--ink-strong); }
.mvi-sub    { font-size: var(--t-base); color: var(--sub); margin: 0; line-height: 1.45; }
.mvi-hint   { font-size: var(--t-sm); color: var(--sub); line-height: 1.45; }
.mvi-label  { font-size: var(--t-2xs); font-weight: 700; letter-spacing: .13em;
              text-transform: uppercase; color: var(--sub-soft); margin: 0; }

/* ── surfaces ─────────────────────────────────────────────────────────────── */
.mvi-card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow: hidden;
}
.mvi-card--flush { border-radius: var(--r-lg); }

/* A list is a card whose children are rows. The divider between rows is --line-soft,
   never --line: an edge inside a container reads as a crack when it is the same weight
   as the container's own border. */
.mvi-list { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg); overflow: hidden; }

.mvi-row {
  display: flex; align-items: center; gap: var(--s-3);
  width: 100%; text-align: left;
  background: transparent; border: 0; border-top: 1px solid var(--line-soft);
  padding: var(--s-3) var(--s-4);
  min-height: var(--tap);
  color: var(--ink-strong); font: inherit; cursor: pointer;
}
.mvi-row:first-child { border-top: 0; }
.mvi-row:hover { background: var(--wash); }
.mvi-row__ic   { flex: 0 0 auto; width: 22px; height: 22px; color: var(--sub); }
.mvi-row__txt  { flex: 1 1 auto; min-width: 0; }
.mvi-row__txt b    { display: block; font-size: var(--t-md); font-weight: 600; line-height: 1.25; }
.mvi-row__txt span { display: block; font-size: var(--t-xs); color: var(--sub); margin-top: 2px; line-height: 1.35; }
.mvi-row__end  { flex: 0 0 auto; display: flex; align-items: center; gap: var(--s-2); }
.mvi-chev      { width: 8px; height: 13px; color: var(--sub-soft); flex: 0 0 auto; }

/* ── controls ─────────────────────────────────────────────────────────────── */
.mvi-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  font: inherit; font-size: var(--t-base); font-weight: 650;
  padding: 10px var(--s-4); min-height: 44px;
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  background: var(--panel); color: var(--ink);
  cursor: pointer;
}
/* HIDDEN HAS TO MEAN HIDDEN. The browser's own `[hidden] { display: none }` is an
   ordinary rule in the UA sheet, and the inline-flex display set on .mvi-btn above
   beats it on specificity -- so until 14 Sep 2026 a button carrying the `hidden` attribute
   rendered exactly like one that did not. Nothing had tripped it, because nothing in
   the tree had hidden an .mvi-btn before; the launcher's Text button was the first, and
   it showed for a client who had declined texts and for a walk-in with nobody to text.
   Anchored on .mvi-btn[hidden] so the leftmost compound still carries an mvi- class,
   and test/inspection/design_system.js now checks that every mvi- class used with
   `hidden` in the tree has a rule like this one. */
.mvi-btn[hidden] { display: none; }
.mvi-btn:hover  { background: var(--wash); }
.mvi-btn[disabled] { opacity: .55; cursor: default; }
/* The one control an inspector taps with a glove while standing up. */
.mvi-btn--tap     { min-height: var(--tap); font-size: var(--t-md); padding: 0 var(--s-5); }
.mvi-btn--primary { background: var(--accent); border-color: var(--accent); color: #fff; }
/* ON THE GREEN GROUND THE PRIMARY BUTTON INVERTS. The rule above hard-codes white text,
   which is right on every light page and invisible once --accent is itself white. White
   fill with forest text is 9.97:1 and is unmistakably the primary control without
   spending green on furniture. Anchored on .mvi-shell, so it is opt-in like everything
   else here. */
.mvi-shell .mvi-btn--primary {
  background: var(--ink-strong); border-color: var(--ink-strong); color: var(--forest);
}
.mvi-btn--primary:hover { background: var(--accent-d); border-color: var(--accent-d); }
.mvi-btn--danger  { background: var(--danger); border-color: var(--danger); color: #fff; }
.mvi-btn--ghost   { background: transparent; }
.mvi-btn--sm      { font-size: var(--t-xs); padding: 6px var(--s-3); min-height: 34px; }

.mvi-field {
  display: block; width: 100%;
  font: inherit; font-size: 16px;   /* 16px exactly: anything smaller makes iOS zoom the page on focus */
  padding: 11px var(--s-3); min-height: 46px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--panel); color: var(--ink);
}
.mvi-field::placeholder { color: var(--sub-soft); }

/* ── state ────────────────────────────────────────────────────────────────
 * A chip says what something IS; a note says something to the reader. Both take the
 * semantic families, so they read the same way on either surface. */
.mvi-chip {
  font-size: var(--t-2xs); font-weight: 700; letter-spacing: .03em; text-transform: uppercase;
  padding: 3px var(--s-2); border-radius: var(--r-sm); white-space: nowrap;
  background: var(--wash); color: var(--sub);
}
.mvi-chip--good   { background: var(--good-bg);   color: var(--good-ink); }
.mvi-chip--warn   { background: var(--warn-bg);   color: var(--warn-ink); }
.mvi-chip--danger { background: var(--danger-bg); color: var(--danger-ink); }
.mvi-chip--info   { background: var(--info-bg);   color: var(--info-ink); }

.mvi-note {
  font-size: var(--t-sm); line-height: 1.45;
  padding: 10px var(--s-3); border-radius: var(--r-sm);
  border: 1px solid var(--line); background: var(--wash); color: var(--ink);
}
.mvi-note--good   { background: var(--good-bg);   border-color: var(--good-line);   color: var(--good-ink); }
.mvi-note--warn   { background: var(--warn-bg);   border-color: var(--warn-line);   color: var(--warn-ink); }
.mvi-note--danger { background: var(--danger-bg); border-color: var(--danger-line); color: var(--danger-ink); }
.mvi-note--info   { background: var(--info-bg);   border-color: var(--info-line);   color: var(--info-ink); }

.mvi-pill {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--t-2xs); font-weight: 650; white-space: nowrap;
  padding: 5px 10px; border-radius: var(--r-pill);
  background: var(--good-bg); border: 1px solid var(--good-line); color: var(--good-ink);
}
.mvi-pill--warn { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn-ink); }
.mvi-pill--idle { background: var(--wash);    border-color: var(--line);      color: var(--sub); }

.mvi-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: 0 0 auto; }

/* ── page furniture ───────────────────────────────────────────────────────── */
.mvi-bar {
  display: flex; align-items: center; gap: var(--s-3);
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--line);
}
.mvi-bar__id    { flex: 1 1 auto; min-width: 0; }
.mvi-bar__id b    { display: block; font-size: var(--t-md); font-weight: 650; color: var(--ink-strong); }
.mvi-bar__id span { display: block; font-size: var(--t-xs); color: var(--sub); margin-top: 1px; }

/* ── the job strip ────────────────────────────────────────────────────────────
   One line under the open tool's name: which house this tool is working on. Sized to
   be read at arm's length, in sunlight, while holding an iPad in one hand -- so it is
   the same size as the tool's own name rather than a caption under it, and it clips
   with an ellipsis rather than wrapping the bar to two heights mid-walk.

   The wrong-job state is a colour AND a sentence. Colour alone fails in bright sun and
   fails for anyone who cannot separate amber from grey; the words "not today's job" are
   the message and the colour only makes them arrive sooner. */
/* ── THE SCHEDULE'S OFFER, under the launcher's job row ──────────────────────
   Shown only when nothing has been chosen and the schedule has something to suggest, so
   it is a prompt rather than a permanent fixture. Anchored on .mvi-offer* like every
   other component here: a page opts in by adding the class. */
.mvi-offer {
  display: grid; gap: 8px;
  margin: -4px 0 12px; padding: 12px;
  background: var(--wash); border: 1px solid var(--line); border-radius: var(--r-md);
}
.mvi-offer[hidden] { display: none; }
.mvi-offer__head { color: var(--sub); font-size: var(--t-sm); }
/* JUSTIFY-CONTENT, not only justify-items. .mvi-btn is `inline-flex` with
   `justify-content: center`, and the grid switch alone left both lines centred -- the
   address of a house reads as a heading rather than as an address, and two of them stack
   into a poster. Caught by looking at a screenshot; no assertion here was ever going to. */
.mvi-offer__pick {
  display: grid; gap: 2px; justify-items: start; justify-content: start;
  text-align: left; width: 100%; padding: 10px 12px; height: auto;
}
.mvi-offer__pick > span { color: var(--sub); font-size: var(--t-sm); font-weight: 400; }
.mvi-offer__when { color: var(--sub); font-size: var(--t-sm); font-weight: 650; }
.mvi-offer__note { color: var(--sub); font-size: var(--t-sm); }

.mvi-strip {
  display: block; font-size: var(--t-sm); font-weight: 500; color: var(--sub);
  margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mvi-strip--wrong { color: var(--warn); font-weight: 650; }
.mvi-strip[hidden] { display: none; }
.mvi-mark {
  width: 32px; height: 32px; border-radius: var(--r-sm); flex: 0 0 auto;
  display: grid; place-items: center;
  /* A BLUE GRADIENT, AND IT SURVIVED THE ACCENT SWEEP BECAUSE THE SWEEP SKIPPED THIS
     FILE. Both stops -- #1d4ed8 and #0ea5e9 -- were hand-typed and neither was ever a
     token, so the mark stayed blue on a green ground and was the only blue object left on
     the launcher. Caught by looking at the screenshot, which is the third time this week.

     Brand green with the dark ink on it: 5.68:1, both values already tokens, and it does
     NOT compete with the white primary button beside it. A flat fill rather than a
     gradient because a two-stop gradient is two colours nobody named. */
  background: var(--brand);
  font-size: var(--t-xs); font-weight: 800; color: var(--slate-900); letter-spacing: .02em;
}

/* The reading column. 480px is index.html's own .wrap, kept so nothing reflows. */
.mvi-wrap {
  max-width: 480px; width: 100%;
  margin: 0 auto;
  padding: calc(env(safe-area-inset-top, 0px) + var(--s-6)) var(--s-5)
           calc(env(safe-area-inset-bottom, 0px) + var(--s-6));
}

@media (prefers-reduced-motion: reduce) {
  .mvi-app *, .mvi-shell * { transition: none !important; animation: none !important; }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE CLIENT PORTAL — a third surface, on the same vocabulary.

   Two surfaces became three on 15 Sep 2026. The tools are light, the launcher
   shell is dark, and the booking portal is MultiView's own green on a warm
   ground -- because it is the only page in this repository a CLIENT sees, and
   it is where they decide whether to hire a stranger to look at the house they
   are about to buy. It should not look like an internal tool.

   IT REBINDS, IT DOES NOT REPLACE. Exactly what .mvi-shell does for the dark
   ground: the token names stay, the values change, and every component above
   works inside it with no variant of its own. A .mvi-btn--primary is green in
   here and blue in the tools, and neither needed to know.

   PURELY ADDITIVE, AND THAT MATTERS MORE THAN USUAL. This file is shared with
   the field-tools session (claude/file-ownership-handoff.md). EVERY SELECTOR
   BELOW IS ANCHORED TO A .mvi-portal CLASS -- including the few that end in a
   bare tag, like `.mvi-portal__brand img`, which can only ever match inside one
   -- so nothing outside a portal page can be reached by any of it, and no page
   of theirs can move because of anything here. A suite checks that claim rather
   than this paragraph carrying it.

   THE PALETTE IS SAMPLED, NOT CHOSEN. #1D4B33 and #F0EFEB were read
   programmatically out of the revised mockups rather than eyeballed; the first
   concept carried two greens and the sampling is what caught it.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── THE SAME BOX MODEL .mvi-app GETS, AND FOR THE SAME REASON ──────────────
   border-box is declared on `.mvi-app *` and nowhere else, so it covers the
   tools and has never covered anything else. Without it a .mvi-field is
   width:100% PLUS 12px of padding either side plus a border, which overflows
   its column by 26px -- and in a three-column row that pushed the ZIP field out
   through the card's right padding and up against the border.

   It was doing that on the old booking page too, in a narrower container.
   Rebuilding the page is only what made anybody look: there is nothing to
   assert about a wrong margin, and this file's own header says so -- the guard
   that fits a stylesheet is a screenshot. This one came from one. */
.mvi-portal *, .mvi-portal *::before, .mvi-portal *::after { box-sizing: border-box; }

.mvi-portal {
  /* THE ACCENT USED TO BE DECLARED HERE and is now at :root, where every page gets
     it. Removed rather than left as belt-and-braces: a duplicate that agrees today is
     a second thing to update and a second thing to forget. The reasoning that used to
     live here -- that a surface accent is not the tagline colour -- moved to :root with
     the values, because it is the system's reasoning now and not this block's. */

  /* The warm ground is a BAND, not the page. The page under these cards stays
     white so a form reads as paper. */
  --portal-warm: #f0efeb;   /* hero band, and the footer */
  --portal-mint: #edf3ee;   /* a green-tinted panel: service area, info rows */
  --portal-edge: #dfe4e0;   /* a border with the green in it rather than grey */

  --bg:    #ffffff;
  --panel: #ffffff;
  --line:  var(--portal-edge);

  /* Headings on this surface are nearly black. A client reads a price off this
     page and #1f2937 on white is the tools' body colour, not a display one. */
  --ink-strong: #111511;

  color: var(--ink);
  background: var(--bg);
  font-family: var(--face);
}

/* ── the masthead ───────────────────────────────────────────────────────────
   Logo, name, and the two things a client reaches for when a page confuses
   them: a way back to the website and a phone number. The rule under it is the
   company green and is the only thing separating the masthead from the hero. */
.mvi-portal__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-5);
  background: var(--panel);
  border-bottom: 3px solid var(--accent);
  flex-wrap: wrap;
}
.mvi-portal__brand { display: flex; align-items: center; gap: var(--s-3); }
/* THE BRAND MARK IS NO LONGER A CIRCULAR TILE, 15 Sep 2026. It was a 56px circle
   because the asset was a filled square badge: a green tile with an illegible wordmark
   printed inside it, which a border-radius could crop into an avatar without losing
   anything. Elizabeth's source art replaced that with a transparent MVI mark, and a
   transparent mark in a circle is a squashed mark with its corners clipped -- measured,
   not guessed: the lockup rendered as "SPECTIONS, L". Height-constrained with a free
   width, and no radius, because there is no longer a tile for a radius to act on. */
.mvi-portal__brand img { height: 56px; width: auto; }
.mvi-portal__name {
  font-size: var(--t-xl); font-weight: 700; color: var(--ink-strong);
  line-height: 1.15;
}
.mvi-portal__kicker {
  font-size: var(--t-2xs); letter-spacing: .14em; text-transform: uppercase;
  color: var(--sub);
}
.mvi-portal__headlinks {
  display: flex; align-items: center; gap: var(--s-5);
  font-size: var(--t-base);
}
.mvi-portal__headlinks a { color: var(--accent); text-decoration: none; font-weight: 600; }
.mvi-portal__headlinks a:hover { text-decoration: underline; }

/* ── the hero band ──────────────────────────────────────────────────────────
   One sentence saying where the client is and one saying what happens next.
   The eyebrow is constant across every step; the title changes. */
.mvi-portal__hero {
  background: var(--portal-warm);
  padding: var(--s-5) var(--s-5) var(--s-6);
}
.mvi-portal__eyebrow {
  font-size: var(--t-2xs); letter-spacing: .14em; text-transform: uppercase;
  color: var(--accent); font-weight: 700; margin: 0 0 var(--s-2);
}
.mvi-portal__title {
  font-size: clamp(28px, 5vw, 44px); line-height: 1.05; font-weight: 800;
  color: var(--ink-strong); margin: 0 0 var(--s-2); letter-spacing: -.02em;
}
.mvi-portal__lede { font-size: var(--t-lg); color: var(--ink); margin: 0; }

/* ── THE HERO IS A GREETING, AND A GREETING IS FOR ARRIVING ─────────────────
   Full size on step one, where a client is deciding whether this looks like a
   business. From step two on they are working, and the chrome has to get out of
   the way: the masthead, the hero and the rail together stand about 310px tall,
   and on a 900px window that pushed Continue on the time step to 981px -- below
   the fold, on the screen where somebody is trying to pick an appointment.

   Caught by driving the real page in a browser at 1100x900. Nothing that reads
   markup could have seen it, and the assertion it broke -- "Continue is on the
   first screen, not below thirty days of scrolling" -- was written weeks ago
   about a different layout and was still exactly right.

   The lede stays. It is the sentence that explains why the times on offer are
   the ones on offer. */
.mvi-portal.deep .mvi-portal__hero { padding: var(--s-3) var(--s-5) var(--s-4); }
.mvi-portal.deep .mvi-portal__title { font-size: clamp(22px, 3vw, 28px); }
.mvi-portal.deep .mvi-portal__lede { font-size: var(--t-base); }
.mvi-portal.deep .mvi-portal__eyebrow { display: none; }
.mvi-portal.deep .mvi-portal__steps { padding: var(--s-3) var(--s-5); }
.mvi-portal.deep .mvi-portal__head { padding: var(--s-2) var(--s-5); }
.mvi-portal.deep .mvi-portal__brand img { height: 40px; width: auto; }
.mvi-portal.deep .mvi-portal__name { font-size: var(--t-lg); }

/* ── the step rail ──────────────────────────────────────────────────────────
   Numbered, named and connected, rather than four dots. A client who leaves the
   page and comes back should be able to see how much is left without counting.

   THE CONNECTORS ARE DECORATION and are hidden from a screen reader; the state
   a reader needs is on the step itself via aria-current. */
.mvi-portal__steps {
  display: flex; align-items: center; gap: var(--s-2);
  padding: var(--s-5); flex-wrap: wrap;
}
.mvi-portal__step {
  display: flex; align-items: center; gap: var(--s-3);
  font-size: var(--t-base); color: var(--sub); font-weight: 600;
}
.mvi-portal__step i {
  display: grid; place-items: center; font-style: normal;
  width: 34px; height: 34px; border-radius: var(--r-pill);
  border: 2px solid var(--line); color: var(--sub);
  font-size: var(--t-base); font-weight: 700; flex: none;
}
.mvi-portal__step.on { color: var(--ink-strong); }
.mvi-portal__step.on i { background: var(--accent); border-color: var(--accent); color: #fff; }
.mvi-portal__step.done i { border-color: var(--accent); color: var(--accent); }
.mvi-portal__rule {
  height: 1px; background: var(--line); flex: 1 1 24px; min-width: 16px;
}

/* ── the working area: a card and a rail ────────────────────────────────────
   The rail carries what a client wants to keep seeing while they fill the card
   in -- what it costs so far, and how to reach a person. Below 900px it stacks
   UNDER the card rather than over it, because on a phone the form is the job
   and the summary is the reassurance. */
.mvi-portal__grid {
  display: grid; grid-template-columns: minmax(0, 1fr) 320px;
  gap: var(--s-5); align-items: start;
  padding: 0 var(--s-5) var(--s-6);
}
@media (max-width: 900px) {
  .mvi-portal__grid { grid-template-columns: minmax(0, 1fr); }
  .mvi-portal__rail { order: 2; }
}
.mvi-portal__rail { display: grid; gap: var(--s-4); }

/* A panel in the rail, and the solid-green variant the contact card uses. */
.mvi-portal__aside {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: var(--s-5);
}
.mvi-portal__aside--mint { background: var(--portal-mint); border-color: var(--portal-mint); }
.mvi-portal__aside--solid {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
.mvi-portal__aside--solid a { color: #fff; }
.mvi-portal__aside h3 {
  margin: 0 0 var(--s-2); font-size: var(--t-lg); color: inherit; font-weight: 700;
}

/* A money row: label left, figure right, the total ruled off above it. */
.mvi-portal__line {
  display: flex; justify-content: space-between; gap: var(--s-4);
  padding: var(--s-2) 0; font-size: var(--t-base);
}
.mvi-portal__line--total {
  border-top: 1px solid var(--line); margin-top: var(--s-2); padding-top: var(--s-3);
  font-size: var(--t-lg); font-weight: 700; color: var(--ink-strong);
}

/* ── the two standing promises ──────────────────────────────────────────────
   On every screen in the design, and they are not decoration: the payment line
   answers the question a client is asking at exactly this moment, and the TREC
   number is what makes this advertising rather than an anonymous form. */
.mvi-portal__promise {
  background: var(--portal-mint);
  padding: var(--s-4) var(--s-5);
  display: flex; gap: var(--s-4); align-items: baseline; flex-wrap: wrap;
}
.mvi-portal__foot {
  background: var(--portal-warm);
  padding: var(--s-4) var(--s-5) calc(env(safe-area-inset-bottom, 0px) + var(--s-5));
  display: flex; justify-content: space-between; gap: var(--s-4); flex-wrap: wrap;
  font-size: var(--t-sm); color: var(--sub);
}
.mvi-portal__foot a { color: var(--sub); }

/* ── THE SIGNING FRAME ON THE DONE SCREEN ────────────────────────────────────
   Scoped to .mvi-portal like everything else appended below the shared tokens,
   so the field tools cannot inherit it. (mvi.css is shared -- see
   claude/file-ownership-handoff.md.)

   THE HEIGHT IS THE WHOLE PROBLEM. BoldSign's signing experience is a scrolling
   document with a fixed action bar at its foot, and it is served cross-origin,
   so nothing here can measure it or resize to fit. Too short and the bar sits
   below the frame's own scroll with nothing on screen suggesting there is more;
   too tall and the page's own Continue-below-the-fold problem comes back on the
   one screen that must not have it.

   72vh, floored at 460 and capped at 760. On a 900px laptop that is 648px; on a
   740px phone it is 533px, which still shows the bar. Both are inside the range
   where the frame scrolls itself rather than the page scrolling it. */
.mvi-portal .signframe {
  position: relative;
  height: clamp(460px, 72vh, 760px);
  margin: 10px 0 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--panel);
}
/* ── WAITING FOR THE DOCUMENT, IN THE SPACE IT WILL FILL ────────────────────
   Same box, same height, so the frame does not shove the page down when it
   arrives. A wait that reflows the layout under somebody reading is worse than
   the wait itself. Additive only: the shared stylesheet belongs to both
   sessions, and this adds a modifier rather than touching .signframe. */
.mvi-portal .signframe--waiting {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--sub);
  font-size: var(--t-sm);
  background: var(--wash);
}
.mvi-portal .signframe iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}
.mvi-portal .signframe__h {
  font-size: var(--t-lg);
  font-weight: 600;
  line-height: 1.3;
  margin: 20px 0 4px;
  color: var(--ink-strong);
}

/* Step 5 is not a step: the rail names four and the booking is finished. Taken
   down so the agreement frame starts higher, which matters most on a phone. */
.mvi-portal.done .mvi-portal__steps { display: none; }

/* A PLACEHOLDER IS A PROMPT, NOT A VALUE. The discount box upper-cases what is
   typed into it, and text-transform applies to placeholder text as well — so
   "Enter code" rendered as "ENTER CODE" and read as a label that had been cut
   off rather than as a hint. The transform stays on the field; the prompt is
   exempt. */
.mvi-portal #discountCode::placeholder { text-transform: none; letter-spacing: normal; }

/* The State box is fixed at TX — every package on this page is a TREC-report
   inspection and that licence is Texas-only. Readonly rather than disabled, so
   it still submits and still reads out; styled so it does not invite a click it
   will not answer. */
.mvi-portal #state[readonly] { background: var(--wash); color: var(--sub);
                               cursor: default; }
