/*
 * henry_app.css — the TRANSACTIONAL half of the new design system.
 *
 * WHY THIS EXISTS. Every new-look page shipped to date is a MARKETING page. Measured
 * (selectors, not substrings — `transform` and `information` make a naive grep useless):
 *
 * RULES whose selector targets a form control or a table element (re-measured 2026-08-10 in
 * this session, not carried over from the plan — the plan's numbers came from a different
 * unit and did not reproduce; both controls run, so the counter can say yes AND no):
 *
 *                            form-rules   table-rules
 *     henry_common.css            0            0        <- negative control, must be 0/0
 *     henry_coolgray.css          1            1
 *     henry_fc.css                1            3
 *     sample-kit-cool.css         5            0        (scoped to .request-form, ONE page)
 *     main.css (the OLD sheet)  199           22        <- positive control, must be large
 *
 * ⚠️ That metric counts ELEMENT selectors, so it deliberately UNDER-counts this sheet, whose
 * vocabulary is class-based (.app-input, .app-label). This file defines 110 .app-* classes:
 * 25 form, 27 record/table, 15 checkout, 8 prose, 35 shared shell/button/alert.
 *
 * So the new system had no vocabulary for a form field, a data row, or a checkout step.
 * That is the whole reason the 46 old-look pages could not simply be re-skinned, and it is
 * what this sheet supplies. It is written as PRODUCTION shared CSS, not mockup scaffolding —
 * the mockups in www/legacy-mockups/ are the first consumers, the real pages are the next.
 *
 * FAMILY. Cool-gray, matching every recent wave (FC, Tier-3 industry, overview v2). The
 * :root block below re-declares the cool tokens the way sample-kit-cool.css does, because
 * henry_common.css ships the WARM set (--ink #111, --line #E7E2D8, --soft #F7F6F2) and this
 * sheet loads after it as $page_css.
 *
 * BRAND (silkcards-brand-design.md, followed deliberately):
 *   - Five colours only. Blue #00C2FF is the action colour and must feel earned, not
 *     scattered — in here it is reserved for the primary button, the focus ring, and the
 *     current step. Gold #C9A96E is an accent and never leads.
 *   - No gradient page backgrounds. Solid grounds, dark or light.
 *   - No drop shadows on type.
 *   - Body copy is never centre-aligned. Centre is a statement moment only — which on these
 *     pages means an empty state or a confirmation, and nothing else.
 *   - Manrope carries authority with tight tracking; never below 700 for a headline.
 *     (Was Montserrat until 2026-08-14. Note Manrope's weight axis STOPS AT 800 — a headline
 *      declared 900 anywhere renders at 800, silently, with no error.)
 *
 * OWNERSHIP RULE (project doctrine, learned the hard way): every bare element selector in
 * here ASSERTS its own font-weight. henry_coolgray.css enforces the de-bold standard through
 * an ALLOWLIST of class names, so any selector that stays silent on weight inherits from an
 * external list it is not on — which is exactly how a page drifts. A rule that does not
 * declare a property cannot own it.
 */

:root {
  /* cool-gray family — overrides henry_common.css's warm set */
  --blue:#00C2FF; --blue-dark:#008DBC; --blue-soft:#E9F9FE;
  --gold:#C9A96E; --gold-light:#E8C99A; --gold-soft:#EFF2F5;
  --obsidian:#14181D; --black:#14181D; --charcoal:#1E242C; --charcoal-2:#262D36;
  --ink:#14181D; --body:#424B55; --muted:#7A8590;
  /* --line unified 2026-08-14 (owner-directed): was #DBE0E6 here + 3 sibling cool sheets,
     #E2E7EC in henry_fc + the two overview sheets, #DDE4EC in two scoped blocks. Settled on
     #E2E7EC — the PRODUCT-page value — so the most-viewed surface does not move and the
     lower-traffic sheets come to it. ⚠️ NOT unified: henry_common.css's #E7E2D8 and
     henry_product.css's rgba(0,0,0,.09) are the WARM base that the cool sheets deliberately
     override, and about.css has its own page-scoped palette with a warm cream. Those are
     intent, not drift — flattening them would erase a real distinction. */
  --line:#E2E7EC; --soft:#EDF1F4; --cream:#F4F6F8; --white:#FFFFFF;

  /* app-only tokens — states the marketing sheets never needed */
  --ok:#1E8E5A;      --ok-soft:#E8F5EE;
  --warn:#9A6B12;    --warn-soft:#FDF3E0;
  --danger:#B3261E;  --danger-soft:#FDECEA;
  --field-bg:#F4F6F8;
  --field-radius:12px;

  --max:1240px;
  --radius-xl:44px; --radius-lg:30px; --radius-md:22px; --radius-sm:14px;
  --shadow-soft:0 18px 54px rgba(0,0,0,.08);
  --shadow-card:0 12px 34px rgba(0,0,0,.05);
  --font:Manrope, Helvetica, Arial, sans-serif;
  --serif:Georgia,'Times New Roman',serif;
}

/* ===================================================================================
 * 1. PAGE SHELL — shared by all four looks
 * =================================================================================== */

.app-page { background:var(--cream); min-height:60vh; }

/* Present to a screen reader, absent on screen. Needed wherever a control's label would be
   redundant to a sighted person but is the control's only accessible NAME — a search box next
   to a button that already says "Search" is the first case. Not display:none, which removes it
   from the accessibility tree as well and would leave the input nameless. */
.sr-only {
  position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0;
}

/* The title band. Solid ground, not a gradient (brand). Dark variant for account pages,
   where the band doubles as the "you are signed in" marker. */
.app-head { background:var(--white); border-bottom:1px solid var(--line); }
.app-head-inner { max-width:var(--max); margin:0 auto; padding:44px 24px 38px; }
.app-head .eyebrow {
  color:var(--gold); font-size:11px; font-weight:700; letter-spacing:.22em;
  text-transform:uppercase; margin-bottom:14px;
}
/* Owner 2026-08-11, looking at the checkout look: "font is too bold and possibly too
   large. let's tone it down." These are APPLICATION pages, not marketing pages — a
   marketing hero is competing for attention, whereas a person mid-checkout is trying to
   read a form. So the app scale sits one step below the product-page scale on purpose;
   do not "restore" it to match a marketing page. 900 -> 800, and the top of the clamp
   comes down 46 -> 38. */
.app-head h1 {
  margin:0; color:var(--ink); font-size:clamp(27px,2.6vw,38px);
  font-weight:800; letter-spacing:-.03em; line-height:1.1; max-width:820px;
}
.app-head .app-head-sub {
  margin-top:14px; max-width:620px; color:var(--body);
  font-size:16px; font-weight:400; line-height:1.65;
}
.app-head--dark { background:var(--obsidian); border-bottom-color:rgba(255,255,255,.1); }
.app-head--dark h1 { color:var(--white); }
.app-head--dark .eyebrow { color:var(--gold-light); }
.app-head--dark .app-head-sub { color:rgba(255,255,255,.72); }

/* Breadcrumb / back link sitting above the title */
.app-back {
  display:inline-flex; align-items:center; gap:8px; margin-bottom:16px;
  color:var(--blue-dark); font-size:12px; font-weight:700; letter-spacing:.06em;
  text-transform:uppercase;
}
.app-back:hover { color:var(--ink); }
.app-back span { transition:transform .2s ease; }
.app-back:hover span { transform:translateX(-3px); }

.app-wrap { max-width:var(--max); margin:0 auto; padding:48px 24px 88px; }
.app-wrap--narrow { max-width:760px; }
.app-wrap--tight { padding-top:34px; }

/* ===================================================================================
 * 2. CARDS — the surface every app block sits on
 * =================================================================================== */

.app-card {
  background:var(--white); border:1px solid var(--line);
  border-radius:var(--radius-lg); padding:32px; box-shadow:var(--shadow-card);
}
.app-card + .app-card { margin-top:20px; }
/* …but that gap assumes cards are STACKED. Side by side in an .app-row it pushes the second card
   down and, because the grid row is already sized to the tallest, shortens it by exactly that
   20px — so two equal cards end with a ragged bottom that looks like a stretch bug and is not.
   Measured on request-samples-standby: row 266.6px, second card 246.6px. */
.app-row > .app-card + .app-card { margin-top:0; }
/* ⚠️ THE SAME TRAP FROM THE OTHER SIDE, and the fix is NOT another sibling selector.
   `.app-card + .app-card` only fires for a card that follows another CARD, so on orderhistory the
   order list and the block after it met at exactly the same y (measured: record bottom 1062, next
   card top 1062) and the borders collided. I first "fixed" it with `.app-records + .app-card`,
   which ALSO matched nothing — the real previous sibling is the search `<form>` that wraps the
   list, which I had not looked at. **Two wrong guesses in a row is the tell that a sibling
   combinator is the wrong shape here**: it encodes a claim about what precedes an element, and
   that claim is re-broken by the next page that puts something else there (Positive-Primary
   Encoding — an allowlist of predecessors never closes).
   So the CONTAINER owns the rhythm instead, which is true regardless of what the children are, and
   the card-to-card margin is zeroed underneath it so the two cannot add up to 40px. */
.app-wrap { display:flex; flex-direction:column; gap:20px; }
/* ⚠️ AND THE CHILDREN'S OWN TOP MARGINS ARE ZEROED, because a flex `gap` does NOT collapse with
   them — it ADDS. Measured on request-samples-standby: `.app-btn-row` carries margin-top:26px, so
   the gap alone took that page from 26px to 46px, silently re-spacing a page nobody had asked me
   to touch. With this line the container is the SINGLE source of the vertical rhythm between
   top-level blocks, which is the whole point of moving it off sibling selectors. Children keep
   their margins everywhere else — this is scoped to direct children of .app-wrap only. */
.app-wrap > * { margin-top:0; }
.app-card--pad-sm { padding:24px; }
.app-card-head {
  display:flex; align-items:center; justify-content:space-between;
  gap:14px; margin-bottom:6px;
}
/* Same tone-down as .app-head h1 above — a card heading on a form page is a signpost,
   not a headline. 22/800 -> 20/700. */
.app-card-head h2 {
  margin:0; font-size:20px; font-weight:700; letter-spacing:-.02em;
  line-height:1.2; color:var(--ink);
}
.app-card-head h3 {
  margin:0; font-size:16px; font-weight:700; letter-spacing:-.01em; color:var(--ink);
}
.app-card-sub {
  color:var(--body); font-size:14px; font-weight:400; line-height:1.6; margin:8px 0 22px;
}
.app-card-foot {
  margin-top:24px; padding-top:20px; border-top:1px solid var(--line);
  color:var(--muted); font-size:13px; font-weight:400; line-height:1.6;
}

/* ===================================================================================
 * 3. BUTTONS — same shape as the marketing .btn so the two halves read as one site.
 *    Declared here too, because an app page may load without a marketing sheet.
 * =================================================================================== */

.app-btn {
  display:inline-flex; align-items:center; justify-content:center; gap:9px;
  min-height:50px; padding:14px 26px; border-radius:999px;
  border:1px solid transparent; font-family:var(--font); font-size:14px; font-weight:800;
  letter-spacing:-.01em; cursor:pointer; white-space:nowrap;
  transition:transform .2s ease, box-shadow .2s ease, background .2s ease, border-color .2s ease;
}
.app-btn:hover { transform:translateY(-2px); }
.app-btn-primary { background:var(--blue); color:var(--obsidian); box-shadow:0 14px 30px rgba(0,194,255,.22); }
.app-btn-dark    { background:var(--obsidian); color:var(--white); }
.app-btn-quiet   { background:var(--white); color:var(--ink); border-color:var(--line); }
.app-btn-ghost   { background:transparent; color:var(--blue-dark); border-color:rgba(0,141,188,.3); }
.app-btn-danger  { background:transparent; color:var(--danger); border-color:rgba(179,38,30,.3); }
.app-btn-sm      { min-height:40px; padding:10px 18px; font-size:13px; }
.app-btn-block   { width:100%; }
.app-btn[disabled], .app-btn.is-disabled {
  opacity:.42; cursor:not-allowed; transform:none; box-shadow:none;
}
.app-btn-row { display:flex; flex-wrap:wrap; align-items:center; gap:12px; margin-top:26px; }
.app-btn-row--split { justify-content:space-between; }

/* The money summary at the foot of an order — subtotal, tax, shipping, discount, total — as ONE
   wrapping line rather than a stack. Added by www/orderhistory.php, the first consumer, and named
   rather than written inline because the quote and sample histories foot the same way.
   ⚠️ It exists because the first pass reached for .app-cell-sub, which is display:block: each
   figure took its own line and the neighbouring PO# block then sat alongside the middle of the
   stack, which read as a broken float on a phone. Nothing was overflowing and no count was wrong —
   it was only visible by looking at it. */
/* ── THE PRODUCTION-STATUS POP-OVER ──────────────────────────────────────────────────
 * The panel behind "[View printing status]", filled by get_production_status() from the external
 * tracking proxy. It lives here rather than in a page because www/orderhistory.php carries a
 * standing "TODO: merge get_production_status() with samplehistory.php" — a second consumer is
 * already named, so single-homing it is build-for-N, not speculation.
 *
 * ⛔ THIS IS A PORT OF RULES THAT WERE DELETED BY ACCIDENT, AND THE `display:none` IS THE
 * LOAD-BEARING LINE. The old page defined all of this in an inline <style> block; the rebuild
 * dropped the block along with the sprite CSS it also contained, and nothing complained — the
 * markup and the script both still shipped, so the page looked complete and the pop-over would
 * have opened UNSTYLED and UNPOSITIONED, shoving the row apart. It is invisible at rest (the inner
 * div is empty and collapses), so no screenshot of the page could show it, and it only appears
 * after a click on a link most orders never display. Found by class-check.php reporting
 * `production_status` and `layer` as classes no loaded sheet defines.
 *
 * The three sprites carry MEANING — they are the done / in-progress / not-yet ticks — so they are
 * kept as-is rather than restyled away. All five files verified present under www/i/. */
.production_status { position:relative; }
.production_status ul { list-style:none; margin:0; padding:0; }
.production_status ul li { margin-bottom:0 !important; font-size:12px; }
/* ⚠️ IT OPENS TO THE RIGHT, and the old rule's `left:-345px` is deliberately NOT carried over.
   That value was not a style choice, it was a position relative to WHERE THE ANCHOR USED TO BE:
   the trigger lived in the last column of a six-column grid, hard against the right edge, so the
   panel had to reach back leftwards and its `arrow_right_line.png` pointed right, back at the
   trigger. The rebuild puts the trigger in the LEFT column under the timeline, so the same -345px
   put the panel at left:-278px — measured, off the page — and the arrow would have pointed away
   from the thing it belongs to. Opening rightwards from left:0 is the same relationship, mirrored;
   the directional arrow is dropped rather than shown backwards, and the panel gets its right
   border back now that nothing is tucking into it. */
.production_status .layer {
  width:min(375px, calc(100vw - 40px)); left:0; top:0; position:absolute; z-index:999; display:none;
}
.production_status .layer div {
  min-height:200px; padding:10px 10px 2px;
  border:1px solid var(--line); border-radius:var(--radius-sm);
  box-shadow:var(--shadow-card);
  background:var(--white); text-align:left; font-weight:400; color:var(--body);
}
.production_status .layer div h4 {
  color:var(--blue-dark); background:url("/i/dashed_line.png") 0 12px repeat-x;
  padding:10px 0 0; cursor:pointer; position:absolute; bottom:0; width:90%;
}
.production_status .layer div h5 { color:var(--muted); font-size:10px; }
.production_status .layer div small {
  font-size:9px; display:inline-block; height:25px; line-height:25px; width:135px;
  text-align:right; padding:0 23px 0 0; margin-right:5px; color:var(--muted);
  background:url("/i/options_off.png?thin") right center no-repeat;
}
.production_status .layer div small.ing { background:url("/i/options_ing.png") right center no-repeat; }
.production_status .layer div small.on  { background:url("/i/options_on.png?checked") right center no-repeat; color:var(--blue-dark); }
.production_status .layer div .no-mypart { display:none; color:var(--muted); }

/* A small input, for editing one short value in place rather than filling in a form. Sized down
   from .app-input, which is a 49px form control and is right for a form and much too tall sitting
   inline in a card foot. */
.app-input--sm { min-height:34px; height:34px; padding:0 10px; font-size:13px; width:auto; }

/* The in-place PO# editor in an order's foot: label, current value, and (once revealed) a small
   input and its Save, all on ONE line.
   ⚠️ Both this and .app-input--sm were written into the page BEFORE they existed in this sheet, so
   they did nothing: the group fell back to display:block and rendered as a 111px vertical stack
   with a 49px input in it. Nothing errored and nothing counted wrong — the state only appears
   after a click, so no screenshot of the page at rest could show it either. Found by
   _utils/legacy-pages/class-check.php, which is what that tool is for. */
.app-po { display:inline-flex; flex-wrap:wrap; align-items:center; gap:6px; }
.app-po .app-cell-sub { display:inline; margin-top:0; }

.app-record-money { display:flex; flex-wrap:wrap; gap:3px 18px; align-items:baseline; }
.app-record-money > span { color:var(--muted); font-size:13px; font-weight:400; }
.app-record-money .app-money-total { color:var(--ink); font-size:15px; font-weight:800; }
/* Centre the row. --split pushes its buttons to opposite EDGES, which reads as "these are
   unrelated" and looks stranded on a wide card; a decision pair (approve / ask for changes)
   belongs together. Owner-directed 2026-08-15 on the HICD approval page. */
.app-btn-row--center { justify-content:center; }

/* A text link inside app copy. Underlined on purpose — in a form, an unmarked link is
   indistinguishable from a label. */
.app-link {
  color:var(--blue-dark); font-weight:700;
  border-bottom:1px solid rgba(0,141,188,.3); transition:color .2s ease, border-color .2s ease;
}
.app-link:hover { color:var(--ink); border-bottom-color:var(--gold); }

/* ===================================================================================
 * 4. FORMS
 *    Grown from .request-form (sample-kit-cool.css) — the one form treatment that shipped —
 *    and widened to everything the real pages need: errors, required marks, radio/checkbox,
 *    fieldsets, help text, and the reCAPTCHA slot the sign-in pages depend on.
 * =================================================================================== */

.app-form { margin:0; }
.app-form fieldset { margin:0 0 28px; padding:0; border:0; }
.app-form legend {
  padding:0; margin-bottom:16px; color:var(--ink);
  font-size:12px; font-weight:800; letter-spacing:.14em; text-transform:uppercase;
}

.app-field { margin-bottom:18px; }
.app-field:last-child { margin-bottom:0; }

.app-row { display:grid; grid-template-columns:1fr 1fr; gap:16px; }
.app-row--thirds { grid-template-columns:1fr 1fr 1fr; }
.app-row--wide-first { grid-template-columns:2fr 1fr; }
.app-field.full { grid-column:1 / -1; }

.app-label {
  display:flex; align-items:baseline; justify-content:space-between; gap:12px;
  margin-bottom:7px;
  color:var(--muted); font-size:10px; font-weight:800; letter-spacing:.13em;
  text-transform:uppercase;
}
/* The required mark is gold, not red: red is reserved for an actual error, and marking
   every required field in the error colour makes a clean form look broken. */
.app-req { color:var(--gold); margin-left:3px; }
.app-label-aside { text-transform:none; letter-spacing:0; font-size:12px; font-weight:700; }

.app-input, .app-select, .app-textarea {
  width:100%; padding:13px 15px; border-radius:var(--field-radius);
  border:1px solid var(--line); background:var(--field-bg); color:var(--ink);
  font-family:var(--font); font-size:15px; font-weight:400; line-height:1.4;
  transition:border-color .18s ease, background .18s ease, box-shadow .18s ease;
}
.app-input::placeholder, .app-textarea::placeholder { color:#9CA3AA; }
.app-input:focus, .app-select:focus, .app-textarea:focus {
  outline:none; border-color:var(--blue); background:var(--white);
  box-shadow:0 0 0 3px rgba(0,194,255,.16);
}
.app-textarea { min-height:120px; resize:vertical; }
.app-select {
  appearance:none;
  /* chevron drawn in CSS — the brand prohibits generic icon sets, and this avoids
     depending on FontAwesome, which the new chrome deliberately does not load */
  background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),
                   linear-gradient(135deg,var(--muted) 50%,transparent 50%);
  background-position:calc(100% - 20px) 50%, calc(100% - 14px) 50%;
  background-size:6px 6px, 6px 6px; background-repeat:no-repeat;
  padding-right:44px;
}
.app-input[readonly], .app-input[disabled] { color:var(--muted); background:var(--soft); }

/* ── BRANDED DROPDOWN ────────────────────────────────────────────────────────────────
 * Owner 2026-08-11: "dropdown needs to be branded." The screenshot was the OPEN list —
 * the operating system's own popup, which no CSS can reach. `.app-select` above only
 * ever styles the CLOSED control. The only way to brand the open list is to draw it.
 *
 * PORTED, NOT INVENTED. This is the configurator's proven `.cfg-dd*` pattern
 * (henry_configure.css:2329, owner-approved 2026-06-08 for exactly this complaint),
 * re-namespaced so the app pages share one behaviour with the configurator rather than
 * growing a second dropdown that drifts.
 *
 * ⚠️ The native <select> stays in the DOM and stays the source of truth — the drawn list
 * writes to it and fires `change`, so the form POSTs the same value it always did. On
 * touch, the custom layer is not built at all and the native control renders, because a
 * phone's native picker is genuinely better than anything drawn here. Both paths are
 * therefore real; neither is a fallback nobody tests. */
.app-dd { position:relative; }
/* ⛔ THE NATIVE CONTROL IS CLIPPED, NOT display:none, AND THAT IS LOAD-BEARING (2026-09-02 — the root
   cause of GOTCHAS #348). jQuery's :visible is false for a display:none element, and the Google address
   lookup (geo-dropdown.js) resolves ".select_state:visible" inside its own callback to decide which
   state control to write — hide the native with display:none and the state is silently never set. Clipped
   to 1px it stays :visible, invisible, and unreachable by pointer; app-dropdown.js also sets tabindex=-1
   so it is not a second Tab stop. Same technique inc/page/branded-select.php uses on the sample form. */
.app-dd select { position:absolute !important; width:1px !important; height:1px !important; margin:-1px !important;
  padding:0 !important; border:0 !important; overflow:hidden !important; clip:rect(0 0 0 0) !important;
  white-space:nowrap !important; opacity:0 !important; pointer-events:none !important; }
.app-dd-btn {
  display:flex; align-items:center; justify-content:space-between; gap:12px; width:100%;
  padding:13px 15px; border-radius:var(--field-radius);
  border:1px solid var(--line); background:var(--field-bg); color:var(--ink);
  font-family:var(--font); font-size:15px; font-weight:400; line-height:1.4;
  text-align:left; cursor:pointer;
  transition:border-color .18s ease, background .18s ease, box-shadow .18s ease;
}
.app-dd-btn:hover { border-color:var(--blue-dark); }
.app-dd.is-open .app-dd-btn {
  border-color:var(--blue); background:var(--white); box-shadow:0 0 0 3px rgba(0,194,255,.16);
}
.app-dd-btn.is-placeholder { color:#9CA3AA; }
.app-dd-caret {
  flex:0 0 auto; width:9px; height:9px; margin-right:4px;
  border-right:2px solid var(--muted); border-bottom:2px solid var(--muted);
  transform:rotate(45deg) translateY(-2px); transition:transform .18s ease;
}
.app-dd.is-open .app-dd-caret { transform:rotate(225deg) translateY(-2px); }
.app-dd-list {
  position:absolute; top:calc(100% + 6px); left:0; right:0; z-index:40;
  background:var(--white); border:1px solid var(--line); border-radius:12px;
  box-shadow:0 14px 32px rgba(22,18,11,.16);
  padding:6px; max-height:280px; overflow:auto;
}
.app-dd-opt {
  padding:11px 14px; border-radius:8px;
  font-size:15px; font-weight:400; color:var(--ink); cursor:pointer;
}
.app-dd-opt:hover, .app-dd-opt.is-active { background:var(--soft); }
.app-dd-opt.is-sel { background:rgba(0,194,255,.10); font-weight:700; color:var(--blue-dark); }
.app-dd-opt[aria-disabled="true"] { color:var(--muted); cursor:default; }
.app-dd-opt[aria-disabled="true"]:hover { background:transparent; }

/* MULTI-SELECT rows (2026-08-28, the Gallery's stock/thickness filters). A ticked row in a
   multi list has to look like a THING YOU TURNED ON, not like the highlight that marks the
   current row in a single list — otherwise "3 of 7 ticked" is unreadable at a glance. So the
   box is drawn, and the single-select tint is dropped in this mode. The count after each name
   is the page's own markup, not this component's; it is greyed here so the name stays first. */
.app-dd--multi .app-dd-opt { display:flex; align-items:center; gap:10px; white-space:nowrap; }
/* The list is free to be wider than the button it hangs from. The button is capped narrow so
   two of them fit the filter bar, but a row reading "Raised Spot UV — 12" wrapped to two lines
   inside that cap, which made a tidy menu look broken. */
.app-dd--multi .app-dd-list { min-width:max-content; }
.app-dd--multi .app-dd-opt::before {
  content:''; flex:none; width:16px; height:16px; border-radius:4px;
  border:1.5px solid var(--line); background:var(--white);
  transition:border-color .15s ease, background .15s ease;
}
.app-dd--multi .app-dd-opt.is-sel { background:transparent; color:var(--ink); }
.app-dd--multi .app-dd-opt.is-sel::before {
  border-color:var(--blue-dark); background:var(--blue-dark);
  box-shadow:inset 0 0 0 2px var(--white), inset 0 0 0 8px var(--blue-dark);
}
.app-dd--multi .app-dd-opt.is-active { background:var(--soft); }
.app-dd-opt .app-dd-n { margin-left:auto; color:var(--muted); font-weight:400; font-size:12px; }

/* The reset, in the bar's right-hand end beside the one control that can hide its own state.
   Appears only when something is actually on, so it never invites a click that does nothing. */
.app-gal-clear {
  background:none; border:0; padding:4px 2px; cursor:pointer; align-self:center;
  font-family:var(--font); font-size:13px; color:var(--muted); text-decoration:underline;
}
.app-gal-clear:hover { color:var(--ink); }

.app-help { display:block; margin-top:7px; color:var(--muted); font-size:12px; font-weight:400; line-height:1.5; }

/* Validation. The state lives on .app-field so the label, the control and the message all
   respond together — a red border with a grey label reads as decoration, not a message. */
.app-field.is-error .app-input,
.app-field.is-error .app-select,
.app-field.is-error .app-textarea { border-color:var(--danger); background:var(--danger-soft); }
.app-field.is-error .app-label { color:var(--danger); }
.app-field.is-error .app-input:focus { box-shadow:0 0 0 3px rgba(179,38,30,.15); }
.app-error {
  display:flex; align-items:flex-start; gap:7px; margin-top:7px;
  color:var(--danger); font-size:12px; font-weight:700; line-height:1.45;
}
.app-error::before {
  content:"!"; flex:0 0 auto; width:15px; height:15px; margin-top:1px; border-radius:50%;
  background:var(--danger); color:var(--white);
  font-size:10px; font-weight:800; line-height:15px; text-align:center;
}
.app-field.is-ok .app-input { border-color:var(--ok); background:var(--ok-soft); }

/* Checkbox + radio. Native control, restyled — not a hidden input driven by a div, so it
   stays keyboard-operable and screen-reader-correct without extra JS. */
.app-check { display:flex; align-items:flex-start; gap:11px; cursor:pointer; }
.app-check + .app-check { margin-top:12px; }
.app-check input[type=checkbox], .app-check input[type=radio] {
  flex:0 0 auto; width:19px; height:19px; margin:1px 0 0; accent-color:var(--blue); cursor:pointer;
}
.app-check-text { display:block; color:var(--ink); font-size:14px; font-weight:400; line-height:1.5; }
.app-check-text b { font-weight:700; }
.app-check-text .app-help { margin-top:3px; }

/* A choice rendered as a selectable card — used for delivery speed, payment method,
   address book entries. */
.app-choice {
  display:flex; align-items:flex-start; gap:13px; padding:18px;
  border:1px solid var(--line); border-radius:var(--radius-sm); background:var(--white);
  cursor:pointer; transition:border-color .18s ease, box-shadow .18s ease, background .18s ease;
}
.app-choice + .app-choice { margin-top:12px; }
.app-choice:hover { border-color:rgba(0,194,255,.5); }
.app-choice input { flex:0 0 auto; width:19px; height:19px; margin:2px 0 0; accent-color:var(--blue); }
.app-choice-body { flex:1 1 auto; min-width:0; }
.app-choice-title { display:block; color:var(--ink); font-size:15px; font-weight:700; letter-spacing:-.01em; }
.app-choice-note { display:block; margin-top:4px; color:var(--body); font-size:13px; font-weight:400; line-height:1.55; }
.app-choice-price { flex:0 0 auto; color:var(--ink); font-size:15px; font-weight:800; }
.app-choice.is-selected { border-color:var(--blue); background:var(--blue-soft); box-shadow:0 0 0 1px var(--blue) inset; }
/* A live radio card selects itself — no script needed to keep the highlight on the checked one.
   Written as :has() outside any media query on purpose: :has() takes the specificity of its
   argument, and inside a breakpoint it would outrank the plain rule (GOTCHAS #370). Browsers
   without :has() keep the hover ring and the native radio dot, which is legible. Added 2026-09-01,
   legacy-pages-checkout-address-review-and-confirmation, for www/cart_address.php. */
.app-choice:has(> input:checked) { border-color:var(--blue); background:var(--blue-soft); box-shadow:0 0 0 1px var(--blue) inset; }
/* A choice that is a hidden radio's proxy (the pickup point's address row on checkout) keeps
   layout so the form still posts it, but is never drawn. */
.app-choice.is-proxy { display:none; }

/* The form's action row and its secondary escape hatch ("New customer? Sign up"). */
.app-form-actions {
  display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between;
  gap:16px; margin-top:26px;
}
.app-form-aside { color:var(--body); font-size:13px; font-weight:400; }

/* "OR" divider — sign-in pages carry Google/Facebook buttons below the form. */
.app-divider {
  display:flex; align-items:center; gap:16px; margin:30px 0 24px;
  color:var(--muted); font-size:11px; font-weight:800; letter-spacing:.18em; text-transform:uppercase;
}
.app-divider::before, .app-divider::after {
  content:""; flex:1 1 auto; height:1px; background:var(--line);
}
/* ⛔ .app-social MOVED TO henry_common.css 2026-08-27 (`legacy-pages-login-page`).
   It is not an app-only component: the other page offering social sign-in is
   www/sample-request.php, which loads henry_common.css + its OWN cool-gray sheet and
   never loads this file. Leaving the rules here is precisely why that page grew a
   second, private copy of the design (.onetap-* in sample-kit-cool.css) and why
   login.php's rebuild could not see it. Markup is single-homed at
   inc/page/social-login.php — require that, do not re-author the buttons. */

/* reCAPTCHA. The badge/slot has to be accounted for, not discovered later — sign-in,
   registration and both sample requests all gate on it, and the agency CSP whitelists
   its host. Reserve the room and say what it is. */
.app-captcha-note {
  margin-top:18px; color:var(--muted); font-size:11px; font-weight:400; line-height:1.6;
}
.app-captcha-note a { color:var(--muted); border-bottom:1px solid var(--line); }

/* ===================================================================================
 * 5. ALERTS — the pages call display_alert() and get danger / warning / success
 * =================================================================================== */

/* ⚠️ AN ALERT IS PROSE BY DEFAULT AND ONLY BECOMES A FLEX ROW WHEN IT HAS AN ICON.
   This was `display:flex` unconditionally, and 6 of the 13 alerts in these mockups carry no
   icon — they are just text with a <b> and a <code> in it. On those, flex turned every inline
   child into a flex ITEM in a non-wrapping row, so the text ran straight off the side of a
   phone. Worse, henry_common.css sets body{overflow-x:hidden}, so it was CLIPPED rather than
   scrollable: invisible unless measured. Found by the 375px probe on the Pro page — three
   elements at x=436, x=571 and x=724 in a 375px viewport.
   The flex exists purely to sit an icon beside a block of text, so it is now conditional on
   the icon actually being there. min-width:0 on the text block is the companion fix: a flex
   item does not shrink below its own content width unless told to (the default is
   min-width:auto, not 0), and overflow-wrap gives long unbroken tokens like a file path
   somewhere to break.
   ⚠️ @supports fallback keeps the icon layout working on any engine without :has(). */
.app-alert {
  display:block;
  padding:16px 18px; border-radius:var(--radius-sm); border:1px solid;
  font-size:14px; font-weight:400; line-height:1.6; margin-bottom:22px;
  overflow-wrap:anywhere;
}
.app-alert:has(> .app-alert-icon) { display:flex; align-items:flex-start; gap:13px; }
.app-alert > div { min-width:0; overflow-wrap:anywhere; }
@supports not selector(:has(*)) {
  .app-alert { display:flex; align-items:flex-start; gap:13px; }
}
.app-alert p { margin:0; color:inherit; font-weight:400; }
.app-alert p + p { margin-top:8px; }
.app-alert b, .app-alert strong { font-weight:800; }
.app-alert-icon {
  flex:0 0 auto; width:22px; height:22px; border-radius:50%; margin-top:1px;
  display:flex; align-items:center; justify-content:center;
  font-size:12px; font-weight:800; color:var(--white);
}
.app-alert--danger  { background:var(--danger-soft); border-color:rgba(179,38,30,.28); color:#7C1B15; }
.app-alert--danger  .app-alert-icon { background:var(--danger); }
.app-alert--warning { background:var(--warn-soft); border-color:rgba(154,107,18,.3); color:#6B4A0C; }
.app-alert--warning .app-alert-icon { background:var(--warn); }
.app-alert--success { background:var(--ok-soft); border-color:rgba(30,142,90,.28); color:#14603D; }
.app-alert--success .app-alert-icon { background:var(--ok); }
.app-alert--info    { background:var(--blue-soft); border-color:rgba(0,141,188,.26); color:#0A5B77; }
.app-alert--info    .app-alert-icon { background:var(--blue-dark); }

/* ===================================================================================
 * 6. RECORDS — order history, quote history, sample history, proofs
 *
 *    NOT a plain <table>. The real order-history page is an ACCORDION of order cards, each
 *    expanding to a production-progress timeline. A table cannot hold that, and on a phone a
 *    six-column table is unreadable. So the row IS the card, and it reflows instead of
 *    scrolling sideways.
 * =================================================================================== */

.app-records { display:flex; flex-direction:column; gap:14px; }

.app-record {
  background:var(--white); border:1px solid var(--line); border-radius:var(--radius-md);
  overflow:hidden; transition:border-color .2s ease, box-shadow .2s ease;
}
.app-record:hover { border-color:rgba(0,194,255,.45); box-shadow:var(--shadow-card); }
.app-record.is-open { border-color:var(--blue); }

/* Six children: thumb · identity · two figures · status pill · caret.
   ⚠️ The column count must match, or the caret wraps onto its own line. */
.app-record-head {
  display:grid; grid-template-columns:auto 1fr auto auto auto auto; align-items:center;
  gap:20px; padding:20px 22px; cursor:pointer; width:100%; text-align:left;
  background:none; border:0; font-family:var(--font);
}
.app-record-thumb {
  flex:0 0 auto; width:58px; height:58px; border-radius:var(--radius-sm);
  background:var(--soft); border:1px solid var(--line); overflow:hidden;
}
.app-record-thumb img { width:100%; height:100%; object-fit:cover; display:block; }
/* ⚠️ These blocks declare display:block because callers may author them as <span> — a
   record head is often a set of inline elements inside one grid cell, and a margin on an
   inline element is silently ignored, which reads as "the label and the value are stuck
   together" rather than as a CSS bug. Declaring it here makes the tag choice irrelevant. */
.app-record-id {
  display:block;
  color:var(--ink); font-size:16px; font-weight:800; letter-spacing:-.02em;
}
.app-record-meta {
  margin-top:5px; color:var(--muted); font-size:12px; font-weight:400;
  display:flex; flex-wrap:wrap; gap:4px 16px;
}
.app-record-meta b { color:var(--body); font-weight:700; }
.app-record-figure { text-align:right; }
.app-record-figure .app-record-k {
  display:block; color:var(--muted); font-size:10px; font-weight:800; letter-spacing:.12em; text-transform:uppercase;
}
.app-record-figure .app-record-v {
  display:block; margin-top:4px; color:var(--ink); font-size:16px; font-weight:800; letter-spacing:-.02em;
}
.app-record-caret {
  flex:0 0 auto; width:30px; height:30px; border-radius:50%;
  border:1px solid var(--line); display:flex; align-items:center; justify-content:center;
  color:var(--muted); transition:transform .2s ease, border-color .2s ease, color .2s ease;
}
.app-record.is-open .app-record-caret { transform:rotate(180deg); border-color:var(--blue); color:var(--blue-dark); }
.app-record-caret::before {
  content:""; width:7px; height:7px; margin-top:-3px;
  border-right:2px solid currentColor; border-bottom:2px solid currentColor;
  transform:rotate(45deg);
}

.app-record-body { padding:0 22px 24px; border-top:1px solid var(--line); }
/* Actions belonging to the record. .app-record itself carries NO padding — head and body each
   supply their own — so anything added as a third child sits flush against the card's edges.
   Measured on the HICD approval page before this existed: 1px from the buttons to the bottom
   border, i.e. the border itself. The mockup's records were read-only and never needed a footer,
   which is why the gap only appeared once a real page put buttons in one. */
.app-record-foot { padding:0 22px 24px; }
.app-record-body-grid {
  display:grid; grid-template-columns:1.15fr .85fr; gap:34px; padding-top:24px;
}

/* Status pill. Colour carries meaning, so the word does too — a colour-blind reader and a
   greyscale print both still get the state. */
.app-pill {
  display:inline-flex; align-items:center; gap:7px; padding:7px 14px; border-radius:999px;
  font-size:11px; font-weight:800; letter-spacing:.07em; text-transform:uppercase; white-space:nowrap;
}
.app-pill::before { content:""; width:6px; height:6px; border-radius:50%; background:currentColor; }
.app-pill--live    { background:var(--blue-soft); color:#0A5B77; }
.app-pill--done    { background:var(--ok-soft);   color:#14603D; }
.app-pill--hold    { background:var(--warn-soft); color:#6B4A0C; }
.app-pill--stopped { background:var(--danger-soft); color:#7C1B15; }
.app-pill--idle    { background:var(--soft); color:var(--muted); }

/* Production progress. The legacy page renders this as a nested <table> of icon + label +
   tip rows; here it is a list with a spine, which reflows on a phone. */
.app-steps-v { list-style:none; margin:0; padding:0; }
.app-steps-v li { position:relative; padding:0 0 22px 34px; }
.app-steps-v li:last-child { padding-bottom:0; }
.app-steps-v li::before {
  content:""; position:absolute; left:0; top:2px; width:18px; height:18px; border-radius:50%;
  border:2px solid var(--line); background:var(--white);
}
.app-steps-v li::after {
  content:""; position:absolute; left:8px; top:22px; bottom:0; width:2px; background:var(--line);
}
.app-steps-v li:last-child::after { display:none; }
.app-steps-v li.is-done::before { border-color:var(--ok); background:var(--ok); }
.app-steps-v li.is-done::after  { background:var(--ok); }
.app-steps-v li.is-now::before  { border-color:var(--blue); background:var(--blue); box-shadow:0 0 0 4px rgba(0,194,255,.2); }
.app-steps-v .app-step-t { display:block; color:var(--ink); font-size:14px; font-weight:700; letter-spacing:-.01em; }
.app-steps-v li.is-idle .app-step-t { color:var(--muted); font-weight:400; }
.app-steps-v .app-step-d { display:block; margin-top:3px; color:var(--muted); font-size:12px; font-weight:400; line-height:1.5; }

/* A plain key/value block — ship-to address, file list, per-order totals. */
.app-kv { display:grid; grid-template-columns:auto 1fr; gap:9px 20px; }
.app-kv dt { color:var(--muted); font-size:11px; font-weight:800; letter-spacing:.1em; text-transform:uppercase; }
.app-kv dd { margin:0; color:var(--ink); font-size:14px; font-weight:400; line-height:1.55; }

/* When the data really IS tabular (a quote's line items), a real table — but it scrolls
   inside its own box so the page body never scrolls sideways. */
.app-table-scroll { overflow-x:auto; -webkit-overflow-scrolling:touch; }
.app-table { width:100%; border-collapse:collapse; font-size:14px; min-width:520px; }
.app-table th {
  padding:12px 14px; text-align:left; border-bottom:1px solid var(--line);
  color:var(--muted); font-size:10px; font-weight:800; letter-spacing:.12em; text-transform:uppercase;
  white-space:nowrap;
}
.app-table td {
  padding:15px 14px; border-bottom:1px solid var(--line);
  color:var(--ink); font-weight:400; vertical-align:top;
}
.app-table tr:last-child td { border-bottom:0; }
.app-table td.num, .app-table th.num { text-align:right; font-variant-numeric:tabular-nums; }
.app-table td.strong { font-weight:700; }

/* Filter / search bar above a record list */
.app-toolbar {
  display:flex; flex-wrap:wrap; align-items:center; gap:12px;
  margin-bottom:22px; padding:16px 18px;
  background:var(--white); border:1px solid var(--line); border-radius:var(--radius-md);
}
.app-toolbar .app-input, .app-toolbar .app-select { min-height:44px; padding:10px 14px; }
.app-toolbar-search { flex:1 1 240px; min-width:180px; }
.app-toolbar-count { margin-left:auto; color:var(--muted); font-size:13px; font-weight:700; }

/* Pagination */
.app-pager { display:flex; justify-content:center; align-items:center; gap:7px; margin-top:34px; }
.app-pager a, .app-pager span {
  min-width:40px; height:40px; padding:0 12px; border-radius:999px;
  display:inline-flex; align-items:center; justify-content:center;
  border:1px solid var(--line); background:var(--white);
  color:var(--body); font-size:14px; font-weight:700;
  transition:border-color .2s ease, color .2s ease;
}
.app-pager a:hover { border-color:var(--blue); color:var(--ink); }
.app-pager .is-current { background:var(--obsidian); border-color:var(--obsidian); color:var(--white); }
.app-pager .is-gap { border-color:transparent; background:none; color:var(--muted); }

/* Empty state — one of the two places centre alignment is earned. */
.app-empty {
  text-align:center; padding:70px 24px;
  background:var(--white); border:1px dashed var(--line); border-radius:var(--radius-lg);
}
/* margin-inline:auto is load-bearing, not tidiness: henry_common.css:46 caps EVERY h2 at
   max-width:900px, so in a container wider than 900px this heading sat flush left inside a
   centred block — text-align:center centred the text within the 900px box, not the box within
   the card. Invisible until 2026-08-28, when www/gallery.php became the first consumer wide
   enough to bind (1192px empty state, heading centred at 635 instead of 756). */
.app-empty h2 { margin:0 auto 12px; color:var(--ink); font-size:22px; font-weight:800; letter-spacing:-.03em; }
.app-empty p { margin:0 auto; max-width:420px; color:var(--body); font-size:15px; font-weight:400; line-height:1.65; }
.app-empty .app-btn-row { justify-content:center; }

/* ===================================================================================
 * CONFIRMATION — "you just sent us something." The other place centre alignment is earned.
 *
 * ⚠️ THIS IS A LIFT, NOT AN INVENTION. The look already existed and had exactly one consumer:
 * the sample-kit confirmation's page-scoped www/css/sample-kit-thanks-cool.css (.confirm /
 * .check / .sub / .ship-note), built from an approved mockup. Every other confirmation on the
 * site was reaching for something else — register-thanks.php used .app-empty (a DASHED box,
 * which reads as "nothing here" on the happiest screen a customer sees) and quote_checkout.php
 * used the plain .app-head. Three treatments for one moment, because the design lived in a
 * sheet only one page could load. So the LOOK is reproduced here, single-homed, and the next
 * confirmation inherits it instead of inventing a fourth.
 *
 * NOT copied across: the account card and follow-up card, which are that page's own features.
 * Still to adopt it: register-thanks.php, and cart_checkout_thanks.php once it leaves legacy.
 *
 * The mark is a component-owned SHAPE; the page supplies the glyph, so a confirmation that
 * wants something other than a tick is not forced to fight the CSS.
 * =================================================================================== */

.app-confirm { max-width:620px; margin:0 auto; text-align:center; }

.app-confirm-mark {
  width:84px; height:84px; margin:0 auto 26px; border-radius:50%;
  background:var(--blue); display:flex; align-items:center; justify-content:center;
  box-shadow:0 18px 40px rgba(0,194,255,.32);
}
.app-confirm-mark svg { width:42px; height:42px; }

.app-confirm h1 {
  margin:0; color:var(--ink); font-size:clamp(28px,3vw,40px);
  font-weight:800; letter-spacing:-.03em; line-height:1.15;
}
/* The brand's accent half. Author it in ADMIN by wrapping the words in <em> — the article is
   the copy, so the emphasis is the copy-owner's call, not the template's. */
.app-confirm h1 em { color:var(--gold); font-style:normal; }

/* The one number the customer came for. Not admin copy — it is generated at the moment of
   the request, so the template owns it. */
.app-confirm-ref {
  display:inline-flex; align-items:baseline; gap:10px; margin:26px 0 0;
  padding:13px 22px; border-radius:999px;
  background:var(--white); border:1px solid var(--line); box-shadow:var(--shadow-card);
}
.app-confirm-ref .k {
  color:var(--muted); font-size:11px; font-weight:800;
  letter-spacing:.14em; text-transform:uppercase;
}
.app-confirm-ref b {
  color:var(--ink); font-size:19px; font-weight:800; letter-spacing:.02em;
}

/* The same moment, gone wrong. A tick on a failure screen is worse than no mark at all, so a
   consumer that renders BOTH outcomes through one block (register-thanks.php) switches on this
   rather than branching its whole layout. Neutral, not alarming: the page's own copy carries the
   bad news, and a red siren on "reCaptcha failed" overstates a retry. */
.app-confirm--problem .app-confirm-mark { background:var(--soft); box-shadow:none; }
.app-confirm--problem .app-confirm-mark svg path { stroke:var(--muted); }

.app-confirm .app-prose { margin-top:22px; text-align:center; max-width:none; }
.app-confirm .app-prose p { color:var(--body); font-size:16px; line-height:1.7; }
.app-confirm .app-btn-row { justify-content:center; margin-top:30px; }

.app-confirm-note {
  margin-top:16px; color:var(--muted); font-size:13px; font-weight:700; line-height:1.6;
}

@media (max-width:600px) {
  .app-confirm-mark { width:68px; height:68px; margin-bottom:20px; }
  .app-confirm-mark svg { width:34px; height:34px; }
  /* The pill is inline-flex, so a long reference on a narrow screen would push past the
     viewport rather than wrap — and henry_common.css sets body{overflow-x:hidden}, which
     would CLIP it invisibly rather than let it scroll. Same trap as .app-alert. */
  .app-confirm-ref { display:flex; flex-direction:column; gap:4px; align-items:center; }
}

/* ===================================================================================
 * 7. CHECKOUT
 *    ⚠️ LOOK ONLY. The payment code (inc/payflow.php, inc/authorize_net*.php) and the
 *    #shoppingform POST contract are out of scope by decision. If a rebuild ever needs to
 *    change what a checkout page POSTS, that is a Halt Trigger — stop and ask the owner.
 * =================================================================================== */

/* Step bar. Blue marks the CURRENT step only — the action colour has to stay earned, so
   completed steps are green and future steps are grey. */
.app-steps {
  display:flex; align-items:center; gap:0; margin-bottom:34px;
  background:var(--white); border:1px solid var(--line); border-radius:999px;
  padding:8px; overflow-x:auto; -webkit-overflow-scrolling:touch;
}
.app-step {
  flex:1 1 0; min-width:132px; display:flex; align-items:center; justify-content:center; gap:9px;
  padding:11px 14px; border-radius:999px; white-space:nowrap;
  color:var(--muted); font-size:13px; font-weight:700; letter-spacing:-.01em;
}
.app-step-n {
  width:22px; height:22px; border-radius:50%; flex:0 0 auto;
  display:flex; align-items:center; justify-content:center;
  background:var(--soft); color:var(--muted); font-size:11px; font-weight:800;
}
.app-step.is-done { color:var(--body); }
.app-step.is-done .app-step-n { background:var(--ok); color:var(--white); }
.app-step.is-now { background:var(--obsidian); color:var(--white); }
.app-step.is-now .app-step-n { background:var(--blue); color:var(--obsidian); }

/* Two-column checkout: work on the left, running total on the right. */
.app-cols { display:grid; grid-template-columns:1fr 372px; gap:24px; align-items:start; }
.app-rail { position:sticky; top:24px; }

.app-summary { background:var(--white); border:1px solid var(--line); border-radius:var(--radius-lg); overflow:hidden; }
.app-summary-head {
  padding:22px 24px 18px; border-bottom:1px solid var(--line);
}
.app-summary-head h2 { margin:0; font-size:18px; font-weight:800; letter-spacing:-.02em; color:var(--ink); }
.app-summary-body { padding:20px 24px; }

/* Cart line item */
.app-line { display:grid; grid-template-columns:64px 1fr auto; gap:14px; align-items:start; }
.app-line + .app-line { margin-top:18px; padding-top:18px; border-top:1px solid var(--line); }
.app-line-thumb {
  width:64px; height:64px; border-radius:var(--radius-sm);
  background:var(--soft); border:1px solid var(--line); overflow:hidden;
}
.app-line-thumb img { width:100%; height:100%; object-fit:cover; display:block; }
.app-line-name { display:block; color:var(--ink); font-size:15px; font-weight:700; letter-spacing:-.01em; line-height:1.3; }
.app-line-spec { display:block; margin-top:6px; color:var(--muted); font-size:12px; font-weight:400; line-height:1.6; }
.app-line-spec b { color:var(--body); font-weight:700; }
.app-line-price { display:block; color:var(--ink); font-size:15px; font-weight:800; font-variant-numeric:tabular-nums; white-space:nowrap; }
.app-line-actions { margin-top:10px; display:flex; gap:14px; }
.app-line-actions a { color:var(--muted); font-size:12px; font-weight:700; border-bottom:1px solid var(--line); }
.app-line-actions a:hover { color:var(--danger); border-bottom-color:currentColor; }

/* Quantity stepper */
.app-qty { display:inline-flex; align-items:center; border:1px solid var(--line); border-radius:999px; overflow:hidden; }
.app-qty button {
  width:34px; height:34px; border:0; background:var(--white); color:var(--ink);
  font-size:16px; font-weight:800; cursor:pointer; font-family:var(--font);
}
.app-qty button:hover { background:var(--soft); }
.app-qty input {
  width:52px; height:34px; border:0; border-left:1px solid var(--line); border-right:1px solid var(--line);
  text-align:center; font-family:var(--font); font-size:14px; font-weight:700; color:var(--ink);
  background:var(--white);
}

/* Totals */
.app-totals { list-style:none; margin:0; padding:0; }
.app-totals li {
  display:flex; align-items:baseline; justify-content:space-between; gap:16px; padding:9px 0;
  color:var(--body); font-size:14px; font-weight:400;
}
.app-totals li span:last-child { font-variant-numeric:tabular-nums; color:var(--ink); font-weight:700; }
.app-totals li.is-free span:last-child { color:var(--ok); }
.app-totals li.is-total {
  margin-top:10px; padding-top:16px; border-top:1px solid var(--line);
  color:var(--ink); font-size:17px; font-weight:800;
}
/* Owner flagged the total twice as still too large. 24/900 -> 19/700. It is the most
   important number on the page and it does not need shouting to be found — it is already the
   last row, on its own rule, next to the button. */
.app-totals li.is-total span:last-child { font-size:19px; font-weight:700; letter-spacing:-.02em; }

/* Quantity in the cart is a TIER, not a free number (see the note at the call site), so it
   renders as a select. Narrow, right-aligned under the line price. */
.app-qty-select { max-width:118px; margin-left:auto; padding:8px 12px; font-size:14px; }

/* The optional deadline ask. Hidden until EXPRESS is chosen (owner 2026-08-11) — someone
   paying to go faster has a reason to tell us; someone on Standard has said they do not.
   That targets the ask instead of advertising a free rush to everyone.
   ⚠️ Fails OPEN by design: a browser without :has() support shows the field, which is
   harmless because it is optional. Hiding it by default in CSS and revealing it with JS
   would fail the other way — invisible if the script never runs. */
.app-deadline { display:none; margin-top:22px; padding-top:18px; border-top:1px solid var(--line); max-width:280px; }
.app-ship:has(#mk-ship-exp:checked) .app-deadline { display:block; }
@supports not selector(:has(*)) { .app-deadline { display:block; } }

/* ── FLOW DIAGRAMS + WALLET BUTTONS ──────────────────────────────────────────────────
 * Used only by the checkout-flow exploration (/legacy-mockups/streamline.php), which is a
 * DRAWING of a possible flow, not a page anyone is building yet. Kept in this sheet rather
 * than the scaffolding sheet because if the flow is adopted, a real cart will want the
 * wallet row and the "or" divider — and they should not be re-authored then.
 * ⚠️ The wallet buttons are PICTURES of buttons. No payment SDK, no card field, nothing
 * that touches the Payflow / Authorize.Net path. */
.app-flow { list-style:none; margin:22px 0 0; padding:0; counter-reset:none; }
.app-flow li { display:flex; gap:14px; padding:14px 0; border-top:1px solid var(--line); }
.app-flow li:first-child { border-top:0; padding-top:0; }
.app-flow-n {
  flex:0 0 auto; width:26px; height:26px; border-radius:50%;
  background:var(--soft); border:1px solid var(--line); color:var(--muted);
  font-size:12px; font-weight:700; line-height:24px; text-align:center;
}
.app-flow li.is-block .app-flow-n { background:var(--warn-soft); border-color:var(--warn); color:#6B4A0C; }
.app-flow-b { display:block; }
.app-flow-t { display:block; color:var(--ink); font-size:15px; font-weight:700; }
.app-flow-d { display:block; margin-top:4px; color:var(--body); font-size:13px; line-height:1.6; }

.app-flow-compare { display:grid; grid-template-columns:1fr 1fr; gap:26px; margin-top:20px; }
@media (max-width:760px) { .app-flow-compare { grid-template-columns:1fr; } }
.app-flow-line { margin:10px 0 8px; color:var(--ink); font-size:16px; font-weight:700; }
.app-flow-line span { color:var(--muted); margin:0 6px; font-weight:400; }

.app-wallets { display:flex; flex-wrap:wrap; gap:10px; margin:18px 0 0; }
.app-wallet {
  flex:1 1 140px; min-height:48px; border-radius:10px;
  display:flex; align-items:center; justify-content:center;
  font-size:15px; font-weight:700; letter-spacing:-.01em;
}
.app-wallet--apple  { background:#000; color:#fff; }
.app-wallet--google { background:#fff; color:#3C4043; border:1px solid #DADCE0; }
.app-wallet--paypal { background:#FFC439; color:#003087; }

.app-or {
  display:flex; align-items:center; gap:14px; margin:22px 0 18px;
  color:var(--muted); font-size:11px; font-weight:700; letter-spacing:.12em; text-transform:uppercase;
}
.app-or::before, .app-or::after { content:""; flex:1; height:1px; background:var(--line); }

.app-swap { list-style:none; margin:0; padding:0; }

/* The site's phone control — telephone_fields() in www/global.php — emits a number box, an
   "Ext." label and an extension box with INLINE widths (width:NNpx;display:inline). On the
   payment page (2026-09-01) it sits inside the Form look, so the pair shares one row: the number
   grows, the extension stays narrow. Widths passed by the caller are overridden here on purpose. */
.app-tel > span { display:flex; align-items:center; gap:8px; }
.app-tel input.app-input { width:auto !important; display:block !important; flex:1 1 auto; min-width:0; }
.app-tel input.app-input + label + input.app-input { flex:0 0 72px; }
.app-tel label { margin:0; color:var(--muted); font-size:12px; font-weight:700; white-space:nowrap; }
.app-swap li {
  display:flex; flex-direction:column; gap:2px;
  padding:11px 0; border-top:1px solid var(--line);
  font-size:13px; line-height:1.5;
}
.app-swap li:first-child { border-top:0; padding-top:0; }
.app-swap s { color:var(--muted); }
.app-swap span { color:var(--ink); font-weight:700; }

.app-consider { display:grid; gap:22px; margin-top:20px; }
.app-consider p { margin:0; color:var(--body); font-size:14px; line-height:1.65; }
.app-consider-t { color:var(--ink) !important; font-size:15px !important; font-weight:700; margin-bottom:6px !important; }

/* ── ARTWORK THUMBNAILS + THE PROOF VIEWER ───────────────────────────────────────────
 * silk_basket.thumbnails is a NEWLINE-SEPARATED list (www/orderhistory.php:570), so an
 * order line can carry several images — front, back, a finish layer. The strip is
 * therefore many-by-default; a single-image design would be wrong for the real data.
 * Each stored value is a bare filename OR a full CDN URL and is resolved through
 * awspath('/i/temp/orderthumbnails', …), which is why nothing here hardcodes a path.
 * The .app-thumb-art--* blocks are placeholder ARTWORK for the mockup only; a real page
 * puts an <img> in that slot. */
.app-thumbs { display:flex; flex-wrap:wrap; gap:12px; }
.app-thumbs--stack { flex-direction:column; }
.app-thumb {
  display:block; width:84px; text-decoration:none;
  border:1px solid var(--line); border-radius:var(--radius-sm);
  padding:5px; background:var(--white);
  transition:border-color .18s ease, box-shadow .18s ease;
}
.app-thumbs--stack .app-thumb { display:flex; align-items:center; gap:10px; width:100%; }
.app-thumb:hover { border-color:var(--blue-dark); }
.app-thumb.is-current { border-color:var(--blue); box-shadow:0 0 0 2px rgba(0,194,255,.2); }
.app-thumb-art {
  display:block; width:100%; aspect-ratio:7/4; border-radius:4px; overflow:hidden;
  background:var(--obsidian);
}
.app-thumbs--stack .app-thumb-art { width:64px; flex:0 0 auto; }
/* The real <img> the comment above promises. CONTAIN, not cover: this slot holds a customer's
   ARTWORK, and cropping a business card to fill a 7:4 box hides the edge of the thing they are
   checking. Added by www/orderhistory.php, the first real consumer; single-homed here so the
   next one inherits it instead of writing its own. */
.app-thumb-art img { width:100%; height:100%; object-fit:contain; display:block; }
/* Three distinguishable stand-ins so the strip reads as three DIFFERENT pages rather than
   one image repeated — which is the thing being demonstrated. */
.app-thumb-art--front { background:linear-gradient(135deg,#1A1D21 0%,#2C3238 60%,#8A6D3B 100%); }
.app-thumb-art--back  { background:linear-gradient(135deg,#22272C 0%,#3A424A 100%); }
.app-thumb-art--uv    { background:linear-gradient(135deg,#101418 0%,#1D2A31 55%,#00C2FF 130%); }
.app-thumb-cap {
  display:block; margin-top:5px; color:var(--muted);
  font-size:10px; font-weight:700; letter-spacing:.1em; text-transform:uppercase;
  text-align:center;
}
.app-thumbs--stack .app-thumb-cap { margin-top:0; text-align:left; }

/* THE PROOF PAGE. Rebuilt after reading orderhistory_proof.php:342-345 — the live page prints
   silk_basket.proofs as plain full-width <img> stacked down the page. So this is a column of
   sheets, NOT a viewer with a stage and page thumbnails; that earlier version was invented.
   The actions sit under the sheets because the decision comes after looking. */
.app-proof-sheets { display:flex; flex-direction:column; gap:18px; margin-top:22px; }
.app-proof-sheets figure { margin:0; }
.app-proof-sheets .app-thumb-art {
  width:100%; aspect-ratio:7/4; border-radius:var(--radius-md);
  border:1px solid var(--line);
}
.app-proof-sheets figcaption {
  margin-top:8px; color:var(--muted);
  font-size:11px; font-weight:700; letter-spacing:.12em; text-transform:uppercase;
}
/* A REAL page puts an <img> in these sheets; .app-thumb-art above is the mockup's stand-in and
   its fixed 7/4 is a placeholder convenience. A design proof has no fixed shape — it is whatever
   the designer exported — so height is natural and only the width is constrained. First real
   consumer: orderhistory_hicd_proof.php. orderhistory_proof.php inherits this when it migrates,
   which is why it lives here rather than on that page. */
.app-proof-sheets img {
  display:block; width:100%; height:auto;
  border-radius:var(--radius-md); border:1px solid var(--line); background:var(--white);
}
.app-proof-actions { margin-top:24px; }

/* A file drop target. The live revision form carries a real jquery.fileupload dropzone —
   most revisions are a replacement file, not a description of one. */
.app-drop {
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:4px; padding:28px 20px; text-align:center;
  border:2px dashed var(--line); border-radius:var(--radius-md); background:var(--soft);
}
.app-drop b { color:var(--ink); font-size:14px; }
.app-drop .app-help { margin-top:0; }
/* The real consumer wraps a <label class="app-drop"> around a genuine <input type="file">, so the
   whole box is the click target AND a drop target. The native control is hidden VISUALLY only —
   never `display:none`, which would take it out of the accessibility tree and off the keyboard, and
   jquery.fileupload needs a real focusable input. Added when the first live consumer
   (orderhistory_proof.php) met this component and the raw "Choose Files / No file chosen" widget
   was sitting inside the box the mockup draws as empty. */
.app-drop { cursor:pointer; }
.app-drop input[type="file"] {
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}
.app-drop:focus-within { border-color:var(--blue); }

/* ── ATTACHED FILES: the list beside the drop box ─────────────────────────────────────
 * ⛔ THESE NAMES ARE A CONTRACT, NOT OUR VOCABULARY. inc/page/upload_script.php writes into
 * these exact ids and classes — #filelist_<id> for files chosen but not yet sent,
 * #userfilelist_<id> for files already stored, a `.filecancel` link per row, `.badfilelist` on a
 * rejected type, and #filecount_<id>'s inner <span> which it rewrites to "N files". So this
 * block styles where the shared script already puts things rather than renaming anything.
 * Two consumers: www/orderhistory_proof.php and www/quote.php. BOTH shipped these unstyled,
 * which drew the filename at body size beside a bare "Remove" and read as a heading rather than
 * a row — added 2026-08-28 (legacy-pages-quote-cart) after seeing it, not before.
 * `.app-file-group` was likewise an app-prefixed class with no definition anywhere; it is the
 * wrapper both pages already use, so it gets one here rather than a third private copy. */
.app-file-group { margin-top:14px; }
.filelist > div {
  display:flex; align-items:baseline; gap:10px; flex-wrap:wrap;
  padding:7px 0; border-bottom:1px solid var(--line);
  color:var(--body); font-size:13px; font-weight:400; line-height:1.5;
}
.filelist > div:last-child { border-bottom:0; }
.filelist > div strong { color:var(--ink); font-weight:700; }
.filelist .filecancel { margin-left:auto; font-size:12px; font-weight:700; }
.filelist .badfilelist { color:var(--danger); font-size:12px; font-weight:700; }
.filecount { font-weight:700; color:var(--body); }

/* Key/value run in a row rather than stacked — for a short header of order facts. */
.app-kv--inline { display:flex; flex-wrap:wrap; gap:8px 30px; margin-top:14px; }
.app-kv--inline dt { margin:0; }
.app-kv--inline dd { margin:0 0 0 8px; }

/* ── ACCOUNT DASHBOARD ───────────────────────────────────────────────────────────────
 * Left rail + content, the shape every account area converges on. Only used by the
 * dashboard proposal today; it becomes the real account shell if that is adopted. */
.app-dash { display:grid; grid-template-columns:210px minmax(0,1fr); gap:26px; }
/* ⛔ THE CONTENT COLUMN OWNS ITS VERTICAL RHYTHM, for exactly the reason .app-wrap does 60 lines
   above — and this is the line that was missing (owner-caught 2026-08-31: *"I still don't see the
   space between the tiles"*, twice).
   `.app-wrap`'s `gap:20px` was added when orderhistory's list and the block after it met at the
   same y and their borders collided. But on EVERY account page the content is not a child of
   .app-wrap — it sits one level deeper, in .app-dash's second column — so that gap never reached
   it, and the column is a plain block with no rhythm of its own. The card-to-card margin papers
   over it only when a card happens to follow another CARD; put a <form> or an .app-records between
   them, as every history page does, and the sibling selector matches nothing.
   Measured at 1440px before this line, all four with blocks flush at 0px:
       samplehistory-details · orderhistory-details · quotationhistory-details · myaccount
   account.php read 20px purely by luck — its two blocks are both .app-card.
   Same shape as the fix above: the CONTAINER is the single source of rhythm, so it holds whatever
   the children turn out to be, and the children's top margins are zeroed underneath because a flex
   `gap` does NOT collapse with a margin — it ADDS. Scoped to direct children of the column only. */
.app-dash > :not(.app-dash-nav) { display:flex; flex-direction:column; gap:20px; }
/* BOTH margins, not just the top. A flex `gap` adds to whichever margin faces it, so zeroing only
   margin-top left the rhythm at 20/34/36/38/42px across the account pages — the leftover was each
   block's own margin-BOTTOM (e.g. .app-back's 16px, giving 36). Not touching any more, but five
   different gaps down one column still reads as accidental. The container owns the space between
   top-level blocks; the blocks own none of it. Scoped to DIRECT children, so everything nested
   inside a card keeps its own spacing. */
.app-dash > :not(.app-dash-nav) > * { margin-top:0; margin-bottom:0; }
/* ⛔ minmax(0,1fr), NOT 1fr — the desktop rule above already guards this and the mobile one did
   not, which is a defect the moment any account page puts a .app-table in the content column.
   A grid item defaults to min-width:auto, so it cannot shrink below its content; .app-table has
   min-width:520px, so on a phone the column grew to 520+ and took the whole page sideways with it
   — .app-table-scroll's overflow-x:auto never engaged because the scroller was never squeezed.
   Measured 2026-08-31 at a 375px viewport: quotationhistory.php (live since 2026-08-28) had a
   652px document, and the Pro dashboard 582px, while every account page WITHOUT a table sat at
   exactly 375. ⚠️ quotationhistory's own note claims "the table slides sideways inside its own
   box; the page itself never does" — that claim was false and this is what makes it true. */
@media (max-width:860px) { .app-dash { grid-template-columns:minmax(0,1fr); } }
.app-dash-nav { display:flex; flex-direction:column; gap:2px; }
.app-dash-nav a {
  padding:10px 14px; border-radius:9px; color:var(--body);
  font-size:14px; font-weight:700; text-decoration:none;
}
.app-dash-nav a:hover { background:var(--soft); color:var(--ink); }
.app-dash-nav a.is-current { background:var(--obsidian); color:var(--white); }
@media (max-width:860px) {
  .app-dash-nav { flex-direction:row; flex-wrap:wrap; overflow-x:auto; }
}

/* Sign out is a NAV ITEM in the account rail (owner-decided 2026-08-18, D-27: "Log out can also
   live in the same area"), because the v2 header carries no account menu to put it in.
   It sits after a hairline rather than flush under the last real destination — it is not a place
   you go, and an accidental sign-out is the one mis-click in this menu that costs the customer
   something. Muted at rest so the eye lands on the destinations first. */
.app-dash-sep {
  display:block; height:1px; margin:10px 14px; background:var(--line);
}
.app-dash-nav a.app-dash-out { color:var(--muted); font-weight:600; }
.app-dash-nav a.app-dash-out:hover { background:var(--soft); color:var(--ink); }
@media (max-width:860px) {
  /* The rail is a horizontal strip here, so a full-width rule would slice it in half. */
  .app-dash-sep { height:auto; width:1px; align-self:stretch; margin:0 6px; }
}

/* Quick actions. Four, not nine — a shortcut grid stops being a shortcut when it becomes a
   menu, and every one of these is something we can already do today. */
.app-quick { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:12px; }
.app-quick-a {
  display:block; padding:16px; border-radius:var(--radius-md);
  border:1px solid var(--line); background:var(--white); text-decoration:none;
  transition:border-color .18s ease, box-shadow .18s ease;
}
.app-quick-a:hover { border-color:var(--blue); box-shadow:0 0 0 3px rgba(0,194,255,.12); }
.app-quick-a b { display:block; color:var(--ink); font-size:14px; }
.app-quick-a span { display:block; margin-top:3px; color:var(--muted); font-size:12px; line-height:1.45; }

.app-feed { list-style:none; margin:18px 0 0; padding:0; }
.app-feed li {
  display:flex; align-items:baseline; gap:14px;
  padding:12px 0; border-top:1px solid var(--line);
  color:var(--body); font-size:14px;
}
.app-feed li:first-child { border-top:0; padding-top:0; }
.app-feed li span:nth-child(2) { flex:1; }
.app-feed-w { flex:0 0 92px; color:var(--muted); font-size:12px; }
@media (max-width:560px) {
  .app-feed li { flex-wrap:wrap; gap:4px 14px; }
  .app-feed-w { flex-basis:100%; }
}

.app-cell-sub { display:block; margin-top:3px; color:var(--muted); font-size:12px; font-weight:400; }

/* ── GALLERY — THE FIFTH LOOK ─────────────────────────────────────────────────────────
 * A portfolio page is the one place where the chrome should nearly disappear. So the filter
 * bar is a single thin row and everything below it is work.
 * Filter vocabulary comes from silk_creativedesign.optionsets (a SET column), never invented,
 * so the UI cannot offer a filter the data has no answer for. */
/* STICKY (owner-directed 2026-08-28). .site-nav is position:sticky/top:0/z-index:80, so this
   parks directly under it and sits BELOW it in the stack — a filter bar that scrolls away is
   the one control you want while you are deep in 153 pieces. The background is opaque, not
   translucent: photographs scroll under this and a blurred bar over a dark card was unreadable. */
.app-gal-bar {
  display:flex; align-items:flex-start; justify-content:space-between; gap:18px; flex-wrap:wrap;
  padding:14px 0 16px; border-bottom:1px solid var(--line);
  position:sticky; top:var(--gal-bar-top, 84px); z-index:70; background:var(--cream);
}
.app-gal-sets { display:flex; flex-direction:column; gap:10px; flex:1 1 520px; }
.app-gal-set { display:flex; align-items:baseline; gap:12px; }
.app-gal-setlab {
  width:56px; flex:none; color:var(--muted); font-size:11px; font-weight:700;
  text-transform:uppercase; letter-spacing:.07em; line-height:34px;
}
.app-gal-filters { display:flex; flex-wrap:wrap; gap:8px; }
.app-chip {
  padding:8px 14px; border-radius:999px; cursor:pointer;
  border:1px solid var(--line); background:var(--white); color:var(--body);
  font-family:var(--font); font-size:13px; font-weight:700;
  transition:border-color .18s ease, background .18s ease, color .18s ease;
}
.app-chip:hover { border-color:var(--blue-dark); color:var(--ink); }
.app-chip.is-on { background:var(--obsidian); border-color:var(--obsidian); color:var(--white); }
.app-gal-stock { max-width:170px; }
/* The right-hand end of the gallery bar. Its own row on a phone, where the chips already wrap
   and a side-by-side pair of selects would each be too narrow to read a stock name in. */
.app-gal-tools { display:flex; flex-wrap:wrap; gap:8px; }
@media (max-width:600px) { .app-gal-tools { width:100%; } .app-gal-tools .app-select { flex:1 1 140px; max-width:none; } }
.app-gal-count { margin:14px 0 22px; color:var(--muted); font-size:13px; }

/* ONE PIECE, OPENED. A gallery creates exactly one question — can I have that? — so the panel
   is the picture at size plus the answer, and nothing else. Video pieces are YouTube embeds,
   which is why the stage holds an iframe as readily as an img. */
.app-gal-modal { position:fixed; inset:0; z-index:200; display:flex; align-items:center; justify-content:center; padding:24px; }
/* .app-gal-modal[hidden] retired 2026-08-31 — the global [hidden] rule at the top of
   henry_common.css now covers every element, so a per-class patch is one more thing to forget. */
.app-gal-modal-back { position:absolute; inset:0; background:rgba(16,18,20,.72); backdrop-filter:blur(3px); }
.app-gal-modal-card {
  position:relative; width:min(1040px,100%); max-height:calc(100vh - 48px); overflow:auto;
  background:var(--white); border-radius:var(--radius-md); padding:26px;
  box-shadow:0 24px 70px rgba(0,0,0,.34);
}
.app-gal-modal-x {
  position:absolute; top:12px; right:12px; width:34px; height:34px; border-radius:50%;
  border:1px solid var(--line); background:var(--white); color:var(--body);
  font-size:20px; line-height:1; cursor:pointer;
}
.app-gal-modal-x:hover { border-color:var(--blue-dark); color:var(--ink); }
.app-gal-modal .app-gal-stage { background:var(--soft); }
.app-gal-modal .app-gal-stage img,
.app-gal-modal .app-gal-stage iframe { display:block; width:100%; aspect-ratio:4/3; border:0; object-fit:contain; }
.app-gal-modal .app-card-head h2 { max-width:none; }

@media (max-width:820px) {
  .app-gal-modal { padding:12px; }
  .app-gal-modal-card { padding:18px; }
  .app-gal-detail { grid-template-columns:minmax(0,1fr); gap:18px; }
}
@media (max-width:700px) {
  .app-gal-set { flex-direction:column; align-items:stretch; gap:4px; }
  .app-gal-setlab { line-height:1.6; width:auto; }
  .app-gal-bar { position:static; }   /* a sticky bar this tall would eat a phone screen */
}

/* Two columns of work with a deliberate size variation, because cards are photographed at
   different crops and forcing one aspect ratio wastes the good ones. Not true masonry — a
   grid with an occasional tall cell gets the same effect with none of the script. */
.app-gal { display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); gap:20px; }
.app-gal-item { display:block; text-decoration:none; }
.app-gal-media {
  position:relative; display:block; overflow:hidden;
  border-radius:var(--radius-md); border:1px solid var(--line); background:var(--soft);
}
.app-gal-media .app-thumb-art { aspect-ratio:4/3; border-radius:0; }
.app-gal-item.is-tall .app-gal-media .app-thumb-art { aspect-ratio:3/4; }
/* A REAL page puts an <img> in this slot; the .app-thumb-art blocks above are the mockup's
   stand-ins. CONTAIN, not cover, and the reason is measured rather than aesthetic: the four
   sample products on www/agency_sample_request.php (the first real consumer) measure
   511x294, 510x292, 675x450 and 675x450 — ratios 1.50 to 1.75 against this slot's 1.33 — so
   `cover` would slice up to a quarter of the width off the sides of the very product the
   card exists to show. White ground because product thumbnails are shot on white and several
   are transparent PNGs, which over --soft would read as grey bars rather than a product. */
.app-gal-media img {
  display:block; box-sizing:border-box; width:100%; aspect-ratio:4/3;
  object-fit:contain; background:var(--white); padding:14px;
}
/* BLEED VARIANT — for a grid of PHOTOGRAPHY rather than product thumbnails (www/gallery.php,
   2026-08-28). The contain+padding+white default above is right for shots taken on white, and
   wrong here: the gallery's 153 pieces are lifestyle photographs on dark and textured grounds,
   measured at ratios around 1.56 against this slot's 1.33, so `contain` framed them in white
   bars and the padding drew a second frame inside the card's own border. `cover` crops ~8% off
   each side of a photograph, which is what a gallery tile is supposed to do.
   Kept as a MODIFIER, not a change to the default: the sample-request consumer's reason for
   contain is measured and still true. Two intentional looks, two selectable variants. */
.app-gal--bleed .app-gal-media img { object-fit:cover; padding:0; background:var(--soft); }

.app-gal-item:hover .app-gal-media { border-color:var(--blue); box-shadow:0 0 0 3px rgba(0,194,255,.14); }
.app-gal-play {
  position:absolute; inset:auto auto 12px 12px;
  width:34px; height:34px; border-radius:50%;
  background:rgba(0,0,0,.55); color:var(--white);
  font-size:12px; line-height:34px; text-align:center;
}
.app-gal-meta { display:block; margin-top:10px; }
.app-gal-meta b { display:block; color:var(--ink); font-size:14px; }
.app-gal-meta span { display:block; margin-top:3px; color:var(--muted); font-size:12px; line-height:1.5; }

.app-gal-detail { display:grid; grid-template-columns:minmax(0,1.15fr) minmax(260px,1fr); gap:28px; margin-top:20px; }
@media (max-width:820px) { .app-gal-detail { grid-template-columns:1fr; } }
.app-gal-stage { display:block; border-radius:var(--radius-md); overflow:hidden; border:1px solid var(--line); }
.app-gal-stage .app-thumb-art { aspect-ratio:4/3; border-radius:0; }

/* The two-leg shipping model, side by side. Internal explanation only — it is deliberately not
   customer-facing copy, so it lives in a mockup's explanation area rather than on a rate card. */
.app-legs { display:grid; grid-template-columns:1fr 1fr; gap:18px; margin-top:20px; }
@media (max-width:700px) { .app-legs { grid-template-columns:1fr; } }
.app-leg { padding:18px 20px; border-radius:var(--radius-md); border:1px solid var(--line); background:var(--soft); }
.app-leg-k { display:block; color:var(--muted); font-size:11px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; }
.app-leg .app-flow-line { margin:10px 0 6px; font-size:15px; }

/* A small qualifier beside a title — "Fastest" next to Express. Not a status, so it must not
   borrow the pill colours, which mean something. */
.app-tag {
  display:inline-block; margin-left:8px; padding:2px 7px; border-radius:5px;
  background:var(--soft); border:1px solid var(--line); color:var(--muted);
  font-size:10px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
  vertical-align:middle;
}

/* LOADING SHAPED LIKE THE ANSWER. A spinner says "something is happening"; a skeleton in the
   shape of the rate cards says "prices are arriving here", and the page does not jump when
   they land. prefers-reduced-motion switches the shimmer off — a pulsing block is exactly the
   kind of motion that rule exists for. */
.app-choice.is-skeleton { cursor:default; }
.app-sk {
  display:block; border-radius:6px;
  background:linear-gradient(90deg,var(--soft) 25%,#EDEFF1 37%,var(--soft) 63%);
  background-size:400% 100%; animation:app-sk-pulse 1.4s ease infinite;
}
.app-sk--t { width:38%; height:15px; }
.app-sk--n { width:64%; height:12px; margin-top:9px; }
.app-sk--p { width:56px; height:15px; flex:0 0 auto; }
@keyframes app-sk-pulse { 0% { background-position:100% 50%; } 100% { background-position:0 50%; } }
@media (prefers-reduced-motion: reduce) { .app-sk { animation:none; } }

/* ── A CHOICE MADE BY LOOKING ────────────────────────────────────────────────────────
 * Owner: "for the henry questions, I need the links so he can look at what was there and
 * which he wants from them." So a decision between existing pages is presented as the pages
 * themselves — each option is the live link, not a description of it. An option with no page
 * behind it (a URL we have not built) is deliberately NOT a link: it renders flat and says
 * so, because a dead link in a decision surface reads as a broken page rather than as a
 * hypothetical. */
.app-choicelinks { display:grid; gap:12px; margin-top:18px; }
.app-choicelink {
  display:block; padding:16px 18px; border-radius:var(--radius-md);
  border:1px solid var(--line); background:var(--white); text-decoration:none;
  transition:border-color .18s ease, box-shadow .18s ease;
}
a.app-choicelink:hover { border-color:var(--blue); box-shadow:0 0 0 3px rgba(0,194,255,.12); }
.app-choicelink b { display:block; color:var(--ink); font-size:15px; }
.app-choicelink span { display:block; margin-top:5px; color:var(--body); font-size:13px; line-height:1.6; }
.app-choicelink-go {
  color:var(--blue-dark) !important; font-size:12px !important; font-weight:700;
  letter-spacing:.06em; text-transform:uppercase; margin-top:10px !important;
}
.app-choicelink.is-hypothetical { background:var(--soft); border-style:dashed; }
.app-choicelink.is-hypothetical .app-choicelink-go { color:var(--muted) !important; }

.app-reflinks { display:flex; flex-wrap:wrap; gap:8px 10px; margin:14px 0 0; }
.app-reflinks a {
  padding:7px 12px; border-radius:8px; border:1px solid var(--line);
  background:var(--white); color:var(--blue-dark);
  font-size:13px; font-weight:700; text-decoration:none;
}
.app-reflinks a:hover { border-color:var(--blue); color:var(--ink); }

/* A row of headline figures. Used on the Pro dashboard, where a dealer's first question is a
   count rather than a list. Deliberately four or fewer — a stat row stops being scannable the
   moment it wraps to a second line of numbers. */
.app-stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(130px,1fr)); gap:14px; margin-top:18px; }
/* auto-fit at 130px is right for short numeric values and too tight for a two-WORD one: at six
   tiles across an 890px card each gets ~133px, and "Pro Partner" at 22px wrapped to two lines
   while "Sep 13, 2020" wrapped to three. Measured on myaccount's partner panel, its first real
   consumer with wordy values. A modifier rather than a new floor, because the tight packing is
   correct for the numeric case this component was built for. */
.app-stats--3 { grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); }
.app-stats > div {
  padding:14px 16px; border-radius:var(--radius-md);
  border:1px solid var(--line); background:var(--soft);
}
.app-stat-k { display:block; color:var(--muted); font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; }
.app-stat-v { display:block; margin-top:6px; color:var(--ink); font-size:22px; font-weight:700; letter-spacing:-.02em; }

/* Claiming an account that already exists — the second half of "email creates the account".
   Deliberately calm: it is an offer sitting after the order, not a gate in front of it, so it
   gets no alert colour and no urgency. */
.app-claim {
  margin-top:20px; padding:24px; border-radius:var(--radius-lg);
  background:var(--soft); border:1px solid var(--line);
}
.app-claim-t { margin:0; color:var(--ink); font-size:16px; font-weight:700; }
.app-claim-d { margin:6px 0 0; color:var(--body); font-size:14px; line-height:1.6; }
.app-claim-form { display:flex; gap:10px; margin-top:16px; flex-wrap:wrap; }
.app-claim-form .app-input { flex:1 1 220px; width:auto; }
.app-claim .app-help { margin-top:10px; }

/* Consent ticks directly ABOVE the commit button. Position is the point: an agreement a
   person scrolls past after pressing pay is not an agreement. The terms tick already exists
   on the live checkout (www/cart_checkout.php:391) — this is where it goes, not a new ask.
   Boxed and tinted so the two ticks read as a block that belongs to the button below them. */
.app-consent {
  margin-top:18px; padding:16px 16px 14px; border-radius:12px;
  background:var(--soft); border:1px solid var(--line);
  display:flex; flex-direction:column; gap:12px;
}
.app-consent .app-check-text { font-size:13px; line-height:1.5; }
.app-consent .app-help { margin-top:4px; font-size:11px; }

/* Trust row under the pay button — the one place on checkout that earns reassurance copy. */
.app-trust {
  margin-top:16px; display:flex; flex-direction:column; gap:9px;
  color:var(--muted); font-size:12px; font-weight:400; line-height:1.5;
}
.app-trust span { display:flex; align-items:flex-start; gap:8px; }
.app-trust span::before {
  content:""; flex:0 0 auto; width:14px; height:14px; margin-top:2px; border-radius:50%;
  background:var(--ok-soft); border:1px solid var(--ok);
}

/* ===================================================================================
 * 8. PROSE — terms, privacy, support, FAQ, the Pro reference pages
 *    Scoped to .app-prose so it can never leak into a marketing page's typography.
 * =================================================================================== */

.app-prose { color:var(--body); font-size:16px; font-weight:400; line-height:1.75; max-width:74ch; }
.app-prose > * + * { margin-top:18px; }
.app-prose h2 {
  margin-top:52px; color:var(--ink); font-size:clamp(22px,2.2vw,28px);
  font-weight:800; letter-spacing:-.03em; line-height:1.2; scroll-margin-top:96px;
}
.app-prose h3 {
  margin-top:34px; color:var(--ink); font-size:18px; font-weight:700;
  letter-spacing:-.02em; line-height:1.3; scroll-margin-top:96px;
}
.app-prose h2:first-child, .app-prose h3:first-child { margin-top:0; }
.app-prose p { color:var(--body); font-weight:400; }
.app-prose strong, .app-prose b { color:var(--ink); font-weight:700; }
.app-prose a { color:var(--blue-dark); font-weight:700; border-bottom:1px solid rgba(0,141,188,.3); }
.app-prose a:hover { color:var(--ink); border-bottom-color:var(--gold); }
.app-prose ul, .app-prose ol { margin:18px 0; padding-left:22px; }
.app-prose li { margin-bottom:9px; font-weight:400; }
.app-prose li::marker { color:var(--gold); }
.app-prose blockquote {
  margin:26px 0; padding:4px 0 4px 22px; border-left:3px solid var(--gold);
  font-family:var(--serif); font-style:italic; font-size:18px; color:var(--ink); line-height:1.6;
}
.app-prose hr { margin:44px 0; border:0; border-top:1px solid var(--line); }
.app-prose table { width:100%; border-collapse:collapse; font-size:14px; margin:26px 0; }
.app-prose th {
  padding:11px 14px; text-align:left; background:var(--soft); border:1px solid var(--line);
  color:var(--ink); font-size:11px; font-weight:800; letter-spacing:.1em; text-transform:uppercase;
}
.app-prose td { padding:12px 14px; border:1px solid var(--line); color:var(--body); font-weight:400; }
.app-prose small { color:var(--muted); font-size:13px; font-weight:400; }

/* The admin-only Edit link that getArticle() prepends into an article title, lifted back
   out of the <h1> by app_article_title_parts(). It arrives as Bootstrap's .float-right,
   which this chrome does not load — so it needs its own home or it lands mid-headline.
   $is_local forces admin true on the Studio, so this is what the owner sees every load. */
/* ⛔ THE WRAPPER MUST BE A <div>, NOT A <p>. What getArticle() hands over is itself a <div>,
   and a <div> cannot legally live inside a <p> — the parser closes the paragraph early, so the
   link ends up a SIBLING of the empty wrapper and inherits none of the rules below. It then
   renders at the inline font-size:20px the injected markup carries, looking like page content
   rather than an admin affordance. This shipped that way on privacy-policy and
   terms-and-conditions and was invisible until someone measured the DOM (the wrapper was
   present and correctly styled — it was simply empty). Same class as the nested-<p> FAQ bug.
   The !important below is the one thing that can beat that inline style. */
.app-article-edit { margin-top:18px; font-size:12px; font-weight:700; letter-spacing:.04em; }
.app-article-edit a { color:var(--blue-dark); }
.app-article-edit a:hover { color:var(--ink); }
.app-article-edit .float-right { float:none; font-size:12px !important; }

/* Table of contents for a long legal page. Sticky on desktop, collapses to a plain block.
   ⚠️ top is measured against the SITE NAV, not the viewport. henry_common.css:83 makes
   .site-nav position:sticky; top:0; z-index:80, so a contents list stuck any higher than
   the nav is tall scrolls underneath it and loses its first rows — with no symptom other
   than the list looking short. Re-measure the nav before changing this number. */
.app-toc {
  position:sticky; top:96px; padding:24px;
  background:var(--white); border:1px solid var(--line); border-radius:var(--radius-md);
}
.app-toc h2 {
  margin:0 0 14px; color:var(--muted);
  font-size:10px; font-weight:800; letter-spacing:.14em; text-transform:uppercase;
}
.app-toc ol { list-style:none; margin:0; padding:0; counter-reset:toc; }
.app-toc li { counter-increment:toc; margin-bottom:10px; }
.app-toc a {
  display:flex; gap:10px; color:var(--body); font-size:13px; font-weight:700; line-height:1.45;
  transition:color .18s ease;
}
.app-toc a::before { content:counter(toc,decimal-leading-zero); color:var(--gold); flex:0 0 auto; }
.app-toc a:hover { color:var(--ink); }
.app-toc a.is-current { color:var(--blue-dark); }

.app-prose-cols { display:grid; grid-template-columns:260px 1fr; gap:44px; align-items:start; }

/* A pulled-out note inside prose.
   ⚠️ display:block is LOAD-BEARING, not tidiness. This rule paints a white background and
   22px of padding, and it declared no display — so on a <span> it stayed INLINE, where a
   background paints once per LINE BOX. A multi-line note then drew a stack of white padded
   boxes straight over the text beside it. Owner, 2026-08-13: "the text is jumbled" — it was
   this, on the hub's decision table, where three <span class="app-note"> notes ran 3-5 lines
   each. The class now owns its own type, so it cannot be inline whatever element carries it. */
.app-note {
  display:block;
  padding:22px 24px; border-radius:var(--radius-md);
  background:var(--white); border:1px solid var(--line); border-left:3px solid var(--blue);
}
.app-note h3 { margin:0 0 8px; color:var(--ink); font-size:15px; font-weight:800; letter-spacing:-.01em; }
.app-note p { margin:0; color:var(--body); font-size:14px; font-weight:400; line-height:1.65; }

/* Updated-on stamp */
.app-stamp {
  display:inline-flex; align-items:center; gap:8px; padding:8px 15px; border-radius:999px;
  background:var(--soft); color:var(--muted);
  font-size:11px; font-weight:800; letter-spacing:.09em; text-transform:uppercase;
}

/* A short index of links — all-products, support landing, the Pro product list */
.app-index { display:grid; grid-template-columns:repeat(3,1fr); gap:14px; }
.app-index a {
  display:block; padding:22px; border-radius:var(--radius-md);
  background:var(--white); border:1px solid var(--line);
  transition:transform .2s ease, border-color .2s ease, box-shadow .2s ease;
}
.app-index a:hover { transform:translateY(-3px); border-color:rgba(201,169,110,.5); box-shadow:var(--shadow-card); }
.app-index .app-index-t { display:block; color:var(--ink); font-size:16px; font-weight:700; letter-spacing:-.02em; }
.app-index .app-index-d { display:block; margin-top:7px; color:var(--muted); font-size:13px; font-weight:400; line-height:1.55; }

/* ===================================================================================
 * 8b. HELP CENTRE — the FAQ page (www/faq.php)
 *
 *     A THIRD job inside the plain-content bucket, and it is not shape A, B or C: a set of
 *     admin-authored articles, grouped into categories, that a person SEARCHES. The rail is
 *     navigation to other URLs (one category per page, /pages/faq/<typeid>) — not anchors in
 *     one document — so it is a sibling of .app-toc rather than a reuse of it.
 *
 *     WHY AN ACCORDION AND NOT THE OPEN WALL THE LEGACY PAGE DRAWS. The answers are long —
 *     one category holds 16 of them, several running several hundred words with tables and
 *     images. Open, that is a page nobody scans, and on a phone it is the same unreadable
 *     wall this lane exists to remove. The FAQ look everywhere else on the site is already a
 *     closed <details> card, so this is the site's own vocabulary, not a new idea.
 *
 *     ⚠️ THE ANSWER BODY CARRIES .app-prose AS WELL AS .app-faq-a, DELIBERATELY. The article
 *     body is admin HTML — headings, lists, tables, images — and .app-prose already types all
 *     of that. Re-declaring it here would be a second copy free to drift from the one the
 *     legal pages use. So .app-faq-a owns SPACING only and inherits typography.
 * =================================================================================== */

/* The category rail. Same 260px column as .app-prose-cols, which this page reuses. */
.app-railnav {
  position:sticky; top:96px; padding:22px;
  background:var(--white); border:1px solid var(--line); border-radius:var(--radius-md);
}
.app-railnav h2 {
  margin:0 0 14px; color:var(--muted);
  font-size:10px; font-weight:800; letter-spacing:.14em; text-transform:uppercase;
}
.app-railnav ul { list-style:none; margin:0; padding:0; }
.app-railnav li + li { margin-top:2px; }
.app-railnav a {
  display:flex; align-items:baseline; justify-content:space-between; gap:10px;
  padding:9px 12px; border-radius:10px;
  color:var(--body); font-size:13px; font-weight:700; line-height:1.4;
  transition:background-color .18s ease, color .18s ease;
}
.app-railnav a:hover { background:var(--soft); color:var(--ink); }
/* The current category is the one state a colour has to carry, so it also gets weight and a
   ground — a colour change alone is invisible to a colour-blind reader. */
.app-railnav a.is-current { background:var(--obsidian); color:var(--white); }
.app-railnav a.is-current .app-railnav-n { color:var(--gold-light); }
/* ⚠️ --body, NOT --muted. The count is information a person reads, not decoration: #7A8590 on
   white measures 3.76:1, under the 4.5:1 floor, and at 11px it is exactly the size that needs the
   contrast most. --body is 8.6:1. (The two other sub-4.5 items on this page — .eyebrow at 2.24 and
   the rail's own uppercase label at 3.76 — are INHERITED from the shipped look: the untouched
   terms page measures identically, so they are a sheet-wide decision, not this page's, and are
   filed rather than changed here.) */
.app-railnav-n { flex:0 0 auto; color:var(--body); font-size:11px; font-weight:800; font-variant-numeric:tabular-nums; }

/* Search. A form, not a toolbar — it submits, so the button is real and reachable by keyboard.
   The legacy page hid submission behind an <a onclick> with a FontAwesome glyph, which v2
   chrome does not load, so it rendered as an invisible click target. */
.app-search { display:flex; flex-wrap:wrap; gap:10px; margin-bottom:22px; }
.app-search .app-input { flex:1 1 240px; min-width:0; }
.app-search .app-btn { flex:0 0 auto; }

/* Accordion.
 *
 * ⭐ MEASURED AGAINST THE PRODUCT PAGES, NOT AGAINST THE SHEET I EXPECTED (owner, 2026-08-14:
 * "doesn't look like it fits our branding. Maybe a little more of the blue tones. similar to the
 * product page faq pages"). The first version of this block mirrored the FLAT `<details>` standard
 * in henry_coolgray.css, whose marker is GOLD. **All 10 product pages surveyed render `hub-faq`
 * and ZERO render `<details>`** — so the flat standard is not what a customer sees on a product
 * page, and matching it produced a gold accent where the site uses blue. The values below are read
 * off `#faq .hub-faq .faq-item` in henry_fc.css:763-770.
 *
 * WHAT IS DELIBERATELY NOT COPIED: the mechanism. `hub-faq` is a `div[role=button]` driven by JS.
 * This stays native `<details>` — it is keyboard-accessible and open-able with no script, and this
 * page's SEARCH depends on that (a result set opens every match server-side; a JS accordion would
 * hide every hit if the script never ran). Reproduce the look on our side, not the mechanism.
 *
 * ⚠️ `--blue-dark` IS ALREADY FOUR DIFFERENT VALUES ACROSS THE SYSTEM — #008DBC here and in
 * henry_common.css, #0099CC in henry_fc.css and henry_overview_v2.css, #008DBA in two scoped
 * blocks. This sheet keeps the henry_common value rather than inventing a fifth or editing a
 * shared token from a page rebuild. The blues that carry the LOOK — the border and shadow tints —
 * are `--blue` #00C2FF, which is identical in every sheet, so those match exactly.
 */
.app-faq { display:flex; flex-direction:column; gap:14px; }
.app-faq details {
  background:var(--white); border:1px solid var(--line); border-radius:18px;
  padding:20px 20px 18px; box-shadow:0 10px 28px rgba(20,24,29,.045);
  transition:border-color .2s ease, box-shadow .2s ease, transform .2s ease;
}
.app-faq details:hover {
  border-color:rgba(0,194,255,.7); box-shadow:0 14px 34px rgba(0,194,255,.10);
  transform:translateY(-1px);
}
/* The open card carries the blue too — on this page one answer is open on arrival and every
   search hit is open, so this is the state a person actually spends their time looking at. */
.app-faq details[open] {
  border-color:rgba(0,194,255,.75); box-shadow:0 16px 38px rgba(0,194,255,.11);
}
.app-faq summary {
  display:flex; align-items:flex-start; justify-content:space-between; gap:18px;
  cursor:pointer; list-style:none;
  color:var(--ink); font-size:18px; font-weight:600; letter-spacing:-.02em; line-height:1.3;
}
.app-faq summary::-webkit-details-marker { display:none; }
.app-faq summary::after {
  content:"+"; flex:0 0 auto; width:26px; text-align:center;
  color:var(--blue-dark); font-size:22px; font-weight:700; line-height:1;
  transition:transform .22s ease;
}
.app-faq details[open] > summary::after { content:"\2212"; transform:rotate(180deg); }
.app-faq details:focus-within > summary { outline:2px solid var(--blue); outline-offset:4px; border-radius:4px; }

/* Spacing only — typography comes from .app-prose on the same element. max-width is dropped
   because the card already caps the measure and a second cap leaves a ragged right edge. */
.app-faq-a { margin-top:16px; max-width:none; }
.app-faq-a > :first-child { margin-top:0; }
.app-faq-a img { max-width:100%; height:auto; }
.app-faq-a table { display:block; overflow-x:auto; }

/* Search hit. <mark> is the right element — it survives with meaning if the sheet never
   loads, which a coloured <span> would not. */
.app-faq mark, mark.app-hit { background:var(--blue-soft); color:var(--ink); padding:0 2px; border-radius:3px; }

/* ===================================================================================
 * 9. RESPONSIVE
 *    Every wide thing collapses rather than scrolling the page sideways. The one exception
 *    is .app-table, which scrolls inside its OWN box.
 * =================================================================================== */

@media (max-width:1040px) {
  .app-cols { grid-template-columns:1fr; }
  .app-rail { position:static; }
  .app-prose-cols { grid-template-columns:1fr; gap:28px; }
  .app-toc { position:static; }
  .app-railnav { position:static; }
  /* The rail becomes a wrapping row of chips once it is above the content rather than beside
     it — a stacked 11-item list would push the answers a full screen down the page. */
  .app-railnav ul { display:flex; flex-wrap:wrap; gap:6px; }
  .app-railnav li + li { margin-top:0; }
  .app-railnav a { border:1px solid var(--line); }
  .app-railnav a.is-current { border-color:var(--obsidian); }
  .app-record-body-grid { grid-template-columns:1fr; gap:26px; }
  .app-index { grid-template-columns:1fr 1fr; }
}

@media (max-width:720px) {
  .app-head-inner { padding:32px 20px 28px; }
  .app-wrap { padding:32px 20px 64px; }
  .app-card { padding:24px 20px; border-radius:var(--radius-md); }
  .app-row, .app-row--thirds, .app-row--wide-first { grid-template-columns:1fr; }
  .app-social { grid-template-columns:1fr; }
  .app-index { grid-template-columns:1fr; }
  .app-form-actions { flex-direction:column; align-items:stretch; }
  .app-form-actions .app-btn { width:100%; }
  .app-form-aside { text-align:center; }

  /* The record head stops being a six-column grid and WRAPS — a six-column row on a 375px
     phone is the single worst thing the old pages do.
     ⚠️ It becomes flex, not a narrower grid. A grid with fewer columns than children makes
     the extras spill past the right edge, and because henry_common.css sets
     body{overflow-x:hidden} the spill is CLIPPED rather than scrolled — so the order total
     silently disappears instead of visibly breaking. Measured: two totals reached x=387 in a
     375px viewport before this. */
  .app-record-head {
    display:flex; flex-wrap:wrap; align-items:center; gap:10px 18px; padding:16px;
  }
  .app-record-head > * { flex:0 0 auto; min-width:0; }
  .app-record-head > :nth-child(2) { flex:1 1 150px; }   /* the identity block takes the slack */
  .app-record-figure { text-align:left; }
  .app-record-figure .app-record-v { font-size:15px; }
  .app-record-caret { margin-left:auto; }
  .app-record-body { padding:0 16px 20px; }

  .app-line { grid-template-columns:52px 1fr; }
  .app-line-thumb { width:52px; height:52px; }
  .app-line-price { grid-column:2; margin-top:8px; }

  .app-steps { border-radius:var(--radius-md); }
  .app-step { min-width:112px; font-size:12px; }
}

/* ── THE ADDRESS BOOK ────────────────────────────────────────────────────────────────────────
   Added 2026-08-18 for www/addresses.php. Owner, looking at the first version: "thats going to be
   extremely long. can we make the tiles half a page or smaller? just what is needed."
   A full .app-card per address stacks one screen per address; a customer with six saved addresses
   scrolled six pages. These are compact tiles in a grid instead. */
/* ── THE TILE GRID — a list of small records, each with a heading line, some body lines and a
   row of text-weight controls. Written for Addresses; the SECOND consumer (www/billing.php,
   saved payment methods) proved nothing in it is address-specific, so the neutral .app-tile*
   names are the ones a new consumer should use. .app-addr* is kept on every selector because
   www/addresses.php ships with it — they are the same rules, not a copy, so the two can never
   drift apart. Do not "tidy" this by deleting either half without editing that page. */
.app-addr-grid,
.app-tile-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); gap:14px; }
.app-addr,
.app-tile {
  padding:16px 18px; border-radius:var(--radius-md);
  border:1px solid var(--line); background:var(--white);
}
/* The customer's own short name for this record — "Our studio", "Acme — Dallas" (added 2026-09-08,
   owner-approved; www/addresses.php). It sits ABOVE the name line because it is what the customer
   scans for; the name and company below are what the courier reads.
   ⚠️ Optional by design — most customers never fill it in, and a record without one must render
   exactly as it did before this existed, so there is no min-height, no placeholder and no empty
   box: the element is simply not emitted. Carries the .app-tile-* twin for the same reason every
   rule in this block does — see the note at the top of the block. */
.app-addr-label,
.app-tile-label {
  display:block; margin:0 0 4px;
  font-size:12px; font-weight:700; letter-spacing:.04em; text-transform:uppercase;
  color:var(--blue-dark);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;   /* 48 chars must not wrap the tile */
}
/* The record the customer picked as their default. A left edge rather than a background, so the
   tile still reads as one of a set — a filled panel would make it look like a different KIND of
   thing rather than the same thing, chosen. The "Default" pill carries the meaning; this is only
   what lets the eye find it in a grid without reading. */
.app-addr.is-default,
.app-tile.is-default { border-left:3px solid var(--blue-dark); }

/* ⚠️ flex-wrap ADDED 2026-09-08, when a record could first carry TWO pills at once (Default +
   Business). Flex items shrink before they wrap, so a second pill squeezed the name instead of
   moving itself: "Dana Whitfield" broke onto two lines inside a tile that had room to spare.
   Wrapping puts the pills on their own line and leaves the name whole. It is inert on the
   one-pill and no-pill records, which is every record that existed before today. */
.app-addr-name,
.app-tile-name { display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin:0 0 6px; font-size:15px; color:var(--ink); }
.app-addr-lines,
.app-tile-lines { margin:0; color:var(--body); font-size:13px; line-height:1.5; }
.app-addr-tel,
.app-tile-sub { margin:6px 0 0; color:var(--muted); font-size:12px; }
/* The two controls are text-weight on purpose — a tile carrying two solid buttons reads as the
   page's main action when it is one row of a list. */
.app-addr-act,
.app-tile-act { display:flex; align-items:center; gap:14px; margin-top:12px; }
.app-addr-act form,
.app-tile-act form { display:inline; margin:0; }
.app-addr-act button.app-linkish,
.app-tile-act button.app-linkish {
  padding:0; border:0; background:none; cursor:pointer;
  color:var(--blue-dark); font:inherit; font-size:13px; text-decoration:underline;
}
.app-addr-act button.app-linkish:hover,
.app-tile-act button.app-linkish:hover { color:var(--ink); }

/* The masked number is the card's identity, so it leads the tile and is the one thing set in a
   monospaced face — grouped digits read as digits, not as a word. */
.app-tile-num { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; letter-spacing:.04em; }

/* ── THE ADDRESS LOOKUP ──────────────────────────────────────────────────────────────────────
   Owner: "addresses should all have the autofill which checks for correct addresses and to lessen
   the fields, just like sample request." This is the SAME mechanism www/sample-request.php ships
   (.addr-detail in sample-kit-cool.css) — city / country / state / ZIP are filled by the Google
   lookup and read back as one confirmed line, so the visitor types a street and nothing else.

   ⛔ CLIPPED, NEVER display:none, AND THIS IS LOAD-BEARING. initAddrAutoComplete() is handed the
   STRING "#addrform .select_state:visible" and evaluates it inside its own callback, so the
   matching state control must still have layout boxes at that moment. Under a display:none
   ancestor jQuery's :visible is false, the state is SILENTLY never set, and a wrong state reaches
   a shipping label with nothing on screen to show for it. Same trap, same fix, as sample-request. */
.addr-detail {
  position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}
.addr-detail.is-open {
  position:static; width:auto; height:auto; margin:0;
  overflow:visible; clip:auto; white-space:normal;
}
/* .addr-confirm[hidden] retired 2026-08-31 — this comment's own warning is now a GLOBAL rule at
   the top of henry_common.css ([hidden]{display:none!important}), because the same trap caught a
   third class (.app-tile-grid) and an allowlist of three was clearly never going to close. */
.addr-confirm {
  display:flex; flex-wrap:wrap; align-items:baseline; gap:6px 10px;
  margin:14px 0 0; padding:12px 14px;
  border:1px solid var(--line); border-radius:var(--radius-md); background:var(--soft);
}
.addr-confirm .ac-label { color:var(--muted); font-size:11px; letter-spacing:.08em; text-transform:uppercase; }
.addr-confirm .ac-value { flex:1 1 auto; color:var(--ink); font-size:14px; }
.addr-confirm .ac-edit {
  padding:0; border:0; background:none; cursor:pointer;
  color:var(--blue-dark); font:inherit; font-size:13px; text-decoration:underline;
}
.addr-confirm .ac-edit:hover { color:var(--ink); }

/* ── THE TYPEAHEAD WRAPPER, SINGLE-HOMED AT LAST ─────────────────────────────────────────────
   ⛔ THE ADDRESS FIELD IS NEVER STYLED SHORT — typeahead WRAPS the input at runtime in
   <span class="twitter-typeahead">, and that span is display:inline-block, so it shrink-wraps to
   its content while every other field is a block at its cell's full width. Measured on
   www/addresses.php before this rule: the grid cell was 583px and the input inside it 200px. So it
   is not a width to tune, it is a wrapper to un-shrink, and no width rule on #address alone could
   ever have fixed it.

   ⚠️ THIS IS THE THIRD TIME THIS EXACT BUG HAS SHIPPED, and the reason is always the same: the fix
   existed and was invisible. Legacy pages get it from www/css/main.css (`.twitter-typeahead
   {width:100%}`) — which ONLY header.php loads, so no v2 page inherits it. The owner then caught it
   on the sample request on 2026-08-11 ("Mailing address field is too short, Apt / Suite / Unit is
   too long") and it was fixed in sample-kit-cool.css, a PAGE-SCOPED sheet, scoped further to
   `.request-form`. So the next v2 page with an address lookup — this one — broke identically, and a
   grep for the fix could not find it. It lives here now, on the shared app form, so every account
   page that grows an address field inherits it.

   .tt-input and .tt-hint must stay exactly congruent: .tt-hint is the ghost input drawing the
   inline completion, and any mismatch offsets the hint from what the visitor is typing. */
.app-form .twitter-typeahead { display:block; width:100%; }
.app-form .twitter-typeahead .tt-input,
.app-form .twitter-typeahead .tt-hint { width:100% !important; }

/* A phone number is not a sentence. Two phone boxes at half the form each read as the most
   important thing on it; the address is. */
.app-row--phones { grid-template-columns:1fr 1fr 1fr; }
.app-row--phones > .app-field:nth-child(n+3) { grid-column:1 / -1; }
@media (max-width:760px) { .app-row--phones { grid-template-columns:1fr; } }

/* ── THE SHOPPING CART (www/cart.php, rebuilt 2026-09-01, legacy-pages-shopping-cart-rebuild) ──
   The line markup is the same .app-line quote.php draws; these are the pieces the cart adds.
   The "Quantity: / Paper Stock: / Size:" run inside .app-line-spec is engine output shared with
   quote.php and the review page, written as <span class="label">…</span> <span>…</span>
   <span class="price">…</span> — give the three a look here rather than in each page. */
.app-line-spec .label { color:var(--body); font-weight:700; }
.app-line-spec .price { color:var(--muted); }
.app-line-spec u.bright { text-decoration:none; color:var(--ink); font-weight:700; }
/* A $99-design line shows its brief through inc/page/hicd-option.php — three READ-ONLY
   textareas that partial emits for every consumer (cart, quote, review, confirmation, history).
   The partial is shared with the configurator and never edited per page; styled from outside. */
.app-line-spec textarea[readonly] {
  display:block; width:100%; margin:4px 0 8px; padding:8px 10px; min-height:0;
  border:1px solid var(--line); border-radius:var(--radius-sm); background:var(--soft);
  color:var(--body); font-family:var(--font); font-size:12px; line-height:1.5; resize:vertical;
}
.app-line-unit { display:block; margin-top:4px; color:var(--muted); font-size:12px; font-weight:400; white-space:nowrap; }
.app-line-badge {
  display:inline-block; margin-left:6px; padding:2px 9px; border-radius:999px; vertical-align:middle;
  background:var(--soft); color:var(--muted); font-size:11px; font-weight:700; letter-spacing:0;
}
a.app-line-badge:hover { color:var(--ink); }
/* A quotation's discount row is a line that behaves like a total: no picture, no spec. */
.app-line.is-discount .app-line-thumb { background:transparent; border-style:dashed; }
/* The promo row in the rail is filled by cart.js once a code is accepted; empty, it is not a row. */
.app-totals li.app-totals-promo:has(> span:last-child:empty) { display:none; }
/* The proof choice — two .app-choice cards, tighter than the delivery-speed ones. */
.app-proof .app-label { display:block; margin-bottom:8px; }
.app-proof .app-choice { padding:12px 14px; }
.app-proof .app-choice + .app-choice { margin-top:8px; }
.app-proof .app-choice-title { font-size:14px; }
.app-proof .app-choice-note { font-size:12px; }
/* Owner-directed 2026-09-01: the grey sentence under each proof option shows only for the option that
   is selected. :has() outside any media query on purpose (GOTCHAS #370); a browser without :has()
   shows both notes, which is the previous behaviour. */
.app-proof .app-choice-note { display:none; }
.app-proof .app-choice:has(> input:checked) .app-choice-note { display:block; }
/* A child option (parentid set) is emitted by the engine with class "label ms-3" — a Bootstrap
   margin the v2 chrome does not load; indent it the way quote.php's inline style did. */
.app-line-spec .label.ms-3 { margin-left:1em; }
