/**
 * MCB Productions — Base Layer
 *
 * Document defaults, typography primitives, and the reusable components
 * described in DESIGN.md §5. Layout belongs in a page stylesheet; this
 * file defines what things ARE, not where they go.
 *
 * This file is permanent. It is consumed by the public site, /admin, and
 * the future customer portal.
 *
 * Requires tokens.css to be loaded first.
 */

/* =====================================================================
   1. DOCUMENT
   ===================================================================== */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  /* Anchor targets and skip-link jumps land below the fixed header. */
  scroll-padding-top: var(--space-4xl);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--paper-000);
  color: var(--ink);
  font-family: var(--font-serif);
  font-size: var(--type-body-size);
  font-weight: var(--type-body-weight);
  line-height: var(--type-body-leading);
  /* Literata's default figures are proportional oldstyle in some cuts;
     lining figures keep prose numerals aligned with the sans. */
  font-variant-numeric: lining-nums;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

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

/* `hidden` must win over any component that sets its own `display`.
   Without this, every grid or flex component that a script toggles with
   the attribute stays on screen as an empty box — which is exactly how
   an empty error panel ends up rendered above a perfectly fine form. */
[hidden] {
  display: none !important;
}

/* =====================================================================
   2. FOCUS AND SKIP LINK
   A visible focus indicator on every interactive control is a stated
   accessibility commitment (PRODUCT.md), not a default we inherited.
   ===================================================================== */

:focus-visible {
  outline: 2px solid var(--signal-ink);
  outline-offset: 2px;
  border-radius: var(--radius-hairline);
}

/* Inside a graphite region, signal-ink disappears against the dark
   ground, so the ring lifts to signal-bright. */
.on-graphite :focus-visible,
.site-nav :focus-visible,
.site-footer :focus-visible,
.spec-panel :focus-visible {
  outline-color: var(--signal-bright);
}

.skip-link {
  position: absolute;
  top: var(--space-sm);
  left: var(--space-sm);
  z-index: 100;
  padding: var(--space-md) var(--space-lg);
  background: var(--signal-ink);
  color: var(--paper-000);
  font-family: var(--font-sans);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
  text-decoration: none;
  border-radius: var(--radius-control);
  /* Moved off-screen with transform rather than display:none so it stays
     focusable and animates back in without a layout property. */
  transform: translateY(calc(-100% - var(--space-sm) * 2));
  transition: transform var(--duration-hover) var(--ease-out-quart);
}

.skip-link:focus {
  transform: translateY(0);
}

/* =====================================================================
   3. TYPOGRAPHY
   Serif explains. Sans operates. No monospace anywhere.
   ===================================================================== */

.display {
  margin: 0;
  font-family: var(--font-serif);
  font-size: var(--type-display-size);
  font-weight: var(--type-display-weight);
  line-height: var(--type-display-leading);
  letter-spacing: var(--type-display-tracking);
  /* Prevents a single orphaned word on the last line of a headline. */
  text-wrap: balance;
}

.headline {
  margin: 0;
  font-family: var(--font-serif);
  font-size: var(--type-headline-size);
  font-weight: var(--type-headline-weight);
  line-height: var(--type-headline-leading);
  letter-spacing: var(--type-headline-tracking);
  text-wrap: balance;
}

.title {
  margin: 0;
  font-family: var(--font-serif);
  font-size: var(--type-title-size);
  font-weight: var(--type-title-weight);
  line-height: var(--type-title-leading);
  letter-spacing: var(--type-title-tracking);
}

.lede {
  margin: 0;
  max-width: var(--measure-lede);
  font-family: var(--font-serif);
  font-size: var(--type-lede-size);
  font-weight: var(--type-lede-weight);
  line-height: var(--type-lede-leading);
  text-wrap: pretty;
}

.prose {
  max-width: var(--measure-prose);
}

/* Headings carry their own trailing space inside prose. The type classes
   zero all margins so they can be positioned by their container; inside a
   prose flow that would run a heading straight into its first line. */
.prose h2,
.prose h3,
.prose h4 {
  margin-bottom: var(--space-lg);
}

.prose p {
  margin: 0 0 var(--space-lg);
  text-wrap: pretty;
}

.prose p:last-child {
  margin-bottom: 0;
}

/* The system's connective tissue: the printed label on the case. */
.label {
  display: block;
  font-family: var(--font-sans);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  line-height: var(--type-label-leading);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
}

.ui-text {
  font-family: var(--font-sans);
  font-size: var(--type-ui-size);
  font-weight: var(--type-ui-weight);
  line-height: var(--type-ui-leading);
}

/* Tabular figures with a slashed zero are mandatory on every count,
   dimension, date, and used-sale price. This is what lets the system
   ban monospace without losing column alignment. */
.data {
  font-family: var(--font-sans);
  font-size: var(--type-data-size);
  font-weight: var(--type-data-weight);
  line-height: var(--type-data-leading);
  font-variant-numeric: tabular-nums slashed-zero;
  font-feature-settings: 'tnum' 1, 'zero' 1;
}

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

/* Inline links in prose. Underline offset clears the serif descenders. */
.prose a,
.link {
  color: var(--signal-ink);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: color var(--duration-hover) var(--ease-out-quart);
}

.prose a:hover,
.link:hover {
  color: var(--graphite-900);
}

/* =====================================================================
   4. SHELL
   ===================================================================== */

.shell {
  width: 100%;
  max-width: var(--shell-max);
  margin-inline: auto;
  padding-inline: var(--shell-gutter);
}

.shell-wide {
  width: 100%;
  max-width: var(--shell-wide);
  margin-inline: auto;
  padding-inline: var(--shell-gutter);
}

/* A hairline rule does most of the separating in this system. */
.rule {
  height: 1px;
  border: 0;
  margin: 0;
  background: var(--paper-200);
}

.rule-strong {
  height: 1px;
  border: 0;
  margin: 0;
  background: var(--paper-300);
}

/* =====================================================================
   5. BUTTONS
   Every button translates down one pixel and inverts its shadow on
   :active. A control that does not physically respond to being pressed
   has failed the material premise of the system.
   ===================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  min-height: 52px;
  padding: var(--space-lg) 28px;
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  font-family: var(--font-sans);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  line-height: var(--type-label-leading);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
  text-decoration: none;
  text-align: center;
  cursor: pointer;
  transition:
    background-color var(--duration-hover) var(--ease-out-quart),
    box-shadow var(--duration-hover) var(--ease-out-quart),
    transform var(--duration-press) var(--ease-out-quart);
}

.btn:active {
  transform: translateY(1px);
  box-shadow: var(--shadow-pressed);
}

.btn-primary {
  background: var(--signal-ink);
  color: var(--paper-000);
  box-shadow: var(--shadow-seated);
}

.btn-primary:hover {
  background: var(--graphite-900);
  box-shadow: var(--shadow-raised);
}

.btn-secondary {
  background: var(--paper-000);
  color: var(--ink);
  border-color: var(--paper-300);
  /* Paper is matte: contact shadow only, never the bevel. */
  box-shadow: var(--shadow-paper);
}

.btn-secondary:hover {
  background: var(--paper-100);
  border-color: var(--graphite-500);
}

.btn-ghost {
  min-height: 44px;
  padding: var(--space-md) var(--space-lg);
  background: transparent;
  color: var(--signal-ink);
}

.btn-ghost:hover {
  background: var(--paper-100);
}

.btn:disabled,
.btn[aria-disabled='true'] {
  background: var(--paper-200);
  color: var(--ink-muted);
  border-color: var(--paper-300);
  box-shadow: none;
  cursor: not-allowed;
  transform: none;
}

/* On a graphite ground the secondary button inverts: the paper fill
   would read as a hole punched in the panel. */
.on-graphite .btn-secondary {
  background: transparent;
  color: var(--paper-000);
  border-color: var(--graphite-500);
  box-shadow: var(--bevel);
}

.on-graphite .btn-secondary:hover {
  background: var(--graphite-700);
  border-color: var(--graphite-300);
}

/* =====================================================================
   6. FORM CONTROLS
   Every field carries a visible uppercase label above it. Placeholder
   text is never a substitute for a label.
   ===================================================================== */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.field-label {
  color: var(--ink-muted);
}

.input {
  width: 100%;
  min-height: 52px;
  padding: 14px var(--space-lg);
  background: var(--paper-000);
  color: var(--ink);
  border: 1px solid var(--paper-300);
  border-radius: var(--radius-control);
  font-family: var(--font-sans);
  font-size: var(--type-ui-size);
  line-height: var(--type-ui-leading);
  transition:
    border-color var(--duration-hover) var(--ease-out-quart),
    box-shadow var(--duration-hover) var(--ease-out-quart);
}

.input::placeholder {
  color: var(--ink-muted);
}

/* Border colour alone is insufficient; the ring is required. */
.input:focus-visible {
  border-color: var(--signal-ink);
  border-width: 2px;
  /* Compensate for the thicker border so the text does not shift. */
  padding: 13px 15px;
  outline: 2px solid var(--signal-ink);
  outline-offset: 2px;
}

.input[aria-invalid='true'] {
  border-color: var(--alert);
  border-width: 2px;
  padding: 13px 15px;
}

.input:disabled {
  background: var(--paper-200);
  color: var(--ink-muted);
  cursor: not-allowed;
}

/* A textarea is the same face as an input, sized by its content rather
   than by the 52px control height. Vertical resize only — a horizontally
   resizable field can be dragged out of its own column. */
.input--multiline {
  min-height: 132px;
  padding: var(--space-md) var(--space-lg);
  /* Body leading, not UI leading: this is the one field where the visitor
     writes sentences, and 1.5 sets them too tight to re-read. */
  line-height: var(--type-body-leading);
  resize: vertical;
}

/* Match the border-thickening compensation the single-line field uses, so
   the first character does not shift when the field takes focus. */
.input--multiline:focus-visible,
.input--multiline[aria-invalid='true'] {
  padding: 11px 15px;
}

/* Helper copy under a label. Explains what the field is for or why it is
   worth answering; never a substitute for the label itself. */
.field-hint {
  color: var(--ink-muted);
  font-family: var(--font-sans);
  font-size: 0.875rem;
  line-height: 1.5;
}

/* Optional fields are marked, required ones are not.
   The inverse of the usual asterisk convention, and deliberate: on these
   forms most fields are required, so marking those would put a mark on
   almost every line and mark nothing at all. The visitor PRODUCT.md
   describes is mid-task and anxious, and what actually helps them is
   knowing what they are allowed to skip. */
.field-label-optional {
  margin-left: var(--space-sm);
  color: var(--ink-muted);
  font-weight: 400;
  letter-spacing: var(--type-label-tracking);
  text-transform: none;
}

/* Errors are specific and name what is wrong and how to fix it. */
.field-error {
  color: var(--alert);
  font-family: var(--font-sans);
  font-size: 0.875rem;
  line-height: 1.45;
}

/* ---------------------------------------------------------------------
   6.1 CHOICE CONTROLS — radio and checkbox

   The real input is kept and restyled with `appearance: none` rather than
   hidden behind a decorated sibling. The control keeps its own semantics,
   its own focus behaviour, and its native label association, so nothing
   has to be re-implemented in ARIA and then kept correct.

   Shape carries meaning here and overrides the system's hardware-corner
   rule: round means one of these, square means any of these. A squared
   radio button is a small lie about how the control behaves.
   --------------------------------------------------------------------- */

/* Related controls live in a fieldset; the legend is the group's label. */
.choice-set {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.choice-set-legend {
  /* A legend does not inherit reliably across browsers, so the Label type
     is restated rather than composed. */
  padding: 0;
  color: var(--ink-muted);
  font-family: var(--font-sans);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  line-height: var(--type-label-leading);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
}

.choice-list {
  display: grid;
  gap: var(--space-xs);
  margin-top: var(--space-md);
}

/* Two or three short options sit on one line. Wraps rather than
   overflowing, so a long third option drops instead of scrolling. */
.choice-list--inline {
  grid-auto-flow: column;
  grid-auto-columns: max-content;
  justify-content: start;
  gap: var(--space-xl);
}

@media (max-width: 479px) {
  .choice-list--inline {
    grid-auto-flow: row;
    grid-auto-columns: auto;
    gap: var(--space-xs);
  }
}

/* The whole row is the target, not just the 22px box. */
.choice {
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr);
  gap: var(--space-md);
  align-items: center;
  min-height: 44px;
  padding-right: var(--space-sm);
  font-family: var(--font-sans);
  font-size: var(--type-ui-size);
  line-height: var(--type-ui-leading);
  cursor: pointer;
}

.choice-control {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  width: 22px;
  height: 22px;
  margin: 0;
  background: var(--paper-000);
  border: 1px solid var(--paper-300);
  cursor: pointer;
  transition:
    background-color var(--duration-hover) var(--ease-out-quart),
    border-color var(--duration-hover) var(--ease-out-quart);
}

.choice-control[type='radio']    { border-radius: 50%; }
.choice-control[type='checkbox'] { border-radius: var(--radius-hairline); }

.choice:hover .choice-control {
  border-color: var(--graphite-500);
}

.choice-control:checked {
  background: var(--signal-ink);
  border-color: var(--signal-ink);
}

/* The dot. Drawn rather than a glyph so it renders identically
   everywhere and carries no text semantics. */
.choice-control[type='radio']::after {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--paper-000);
  transform: scale(0);
  transition: transform var(--duration-hover) var(--ease-out-quart);
}

.choice-control[type='radio']:checked::after {
  transform: scale(1);
}

/* The check, drawn from two borders exactly as the What's Included list
   draws its own — one technique for one mark. */
.choice-control[type='checkbox']::after {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  width: 11px;
  height: 6px;
  border-left: 2px solid var(--paper-000);
  border-bottom: 2px solid var(--paper-000);
  transform: rotate(-45deg) translate(1px, -2px) scale(0);
  transition: transform var(--duration-hover) var(--ease-out-quart);
}

.choice-control[type='checkbox']:checked::after {
  transform: rotate(-45deg) translate(1px, -2px) scale(1);
}

.choice-control:focus-visible {
  outline: 2px solid var(--signal-ink);
  outline-offset: 2px;
}

.choice-set[aria-invalid='true'] .choice-control {
  border-color: var(--alert);
}

.choice-control:disabled {
  background: var(--paper-200);
  border-color: var(--paper-300);
  cursor: not-allowed;
}

.choice-control:disabled ~ .choice-text {
  color: var(--ink-muted);
}

@media (prefers-reduced-motion: reduce) {
  .choice-control::after {
    transition: none;
  }
}

/* ---------------------------------------------------------------------
   6.2 FORM ALERT
   A whole-form failure, above the submit control: the server refused the
   submission, or the network did not carry it.

   Bordered on all four sides. A colour-bearing stripe down one edge is
   prohibited by DESIGN.md §6, and it is also the wrong signal — the
   message is the object, not a decorated aside.
   --------------------------------------------------------------------- */

.form-alert {
  display: grid;
  gap: var(--space-sm);
  padding: var(--space-lg);
  background: var(--paper-100);
  border: 1px solid var(--alert);
  border-radius: var(--radius-control);
}

.form-alert-title {
  color: var(--alert);
  font-family: var(--font-sans);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  line-height: var(--type-label-leading);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
}

.form-alert-body {
  margin: 0;
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--type-ui-size);
  line-height: 1.55;
}

/* Field-level problems the server found, listed under the summary. Each
   one already names a specific field and what is wrong with it. */
.form-alert-list {
  margin: 0;
  padding-left: var(--space-xl);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 0.9375rem;
  line-height: 1.55;
}

.form-alert-list li + li {
  margin-top: var(--space-xs);
}

/* The alert takes focus when it appears, so it needs to be focusable
   without joining the tab order. It never shows a ring of its own: the
   scroll and the announcement are the notification. */
.form-alert:focus {
  outline: none;
}

/* =====================================================================
   7. STATUS CHIPS
   Squared off, like a printed inventory tag. Every chip pairs its colour
   with a word — colour alone never conveys state. Chips are never blue;
   blue is reserved for action and a chip is a readout.

   NOTE: not currently used on the public site. The inventory table has
   no quantity column, so stock state cannot yet be substantiated. Kept
   here because the future portal will use it for quote status.
   ===================================================================== */

.chip {
  display: inline-flex;
  align-items: center;
  padding: 6px 10px;
  background: var(--paper-100);
  border: 1px solid currentColor;
  border-radius: var(--radius-hairline);
  font-family: var(--font-sans);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-tracking);
  line-height: var(--type-label-leading);
  text-transform: uppercase;
}

.chip-available { color: var(--available); }
.chip-caution   { color: var(--caution); }
.chip-alert     { color: var(--alert); }

/* =====================================================================
   8. SURFACES
   Paper is matte and gets a hairline rule. Graphite is machined and gets
   the bevel. Nesting a card inside a card is prohibited; use an inset
   band instead.
   ===================================================================== */

.surface-paper {
  background: var(--paper-000);
  border: 1px solid var(--paper-200);
  border-radius: var(--radius-panel);
  box-shadow: var(--shadow-paper);
}

.surface-graphite {
  background: var(--graphite-900);
  color: var(--paper-200);
  border-radius: var(--radius-panel);
  box-shadow: var(--shadow-seated);
}

.surface-graphite .title,
.surface-graphite .headline,
.surface-graphite .display {
  color: var(--paper-000);
}

/* The replacement for a nested card. */
.inset-band {
  background: var(--paper-100);
  border: 1px solid var(--paper-200);
  border-radius: var(--radius-control);
  padding: var(--space-xl);
}

/* =====================================================================
   8.1 PLATE — the editorial photograph
   The component that governs how large a photograph on a public page is
   allowed to be, and the only place that answer is written down. Before
   it existed, seven page stylesheets each invented their own answer and
   the site carried nine unrelated treatments of the same object.

   HEIGHT IS NEVER SET HERE. A plate's height comes from the ratio baked
   into its source file, because the crop lives in the source and a CSS
   aspect-ratio laid over a baked crop is a second, undocumented editorial
   decision. What a role sets is the width band — which column the figure
   occupies — and the frame around it.

   Three roles, chosen by the column the figure sits in, never by the
   photograph:

     .plate-full   Spans the content shell. One per page. Sources baked at
                   1.85:1 or wider, so the height lands 560-620px against
                   a 1144px shell. Takes the `case` radius, because
                   DESIGN.md §4 reserves 16px for the largest framed
                   region on a page and this role is that region.
     .plate-half   One column of a two-column row, around 545px. Sources
                   baked 3:2 to 5:3, so the height lands 330-390px. These
                   are used in pairs.
     .plate-inset  Seated inside a text column and capped, for a figure
                   that annotates a sentence rather than carrying a
                   section.

   THE PROPORTION RULE. A plate set BESIDE a text block runs to at least
   75% of that block's height. Satisfy it by choosing the role that fits
   the column — never by cropping the photograph to fill a gap. A figure
   that cannot reach 75% beside its text belongs below the text as a
   .plate-full instead. The rule exists because the production page
   shipped a 2:1 source in a 0.85fr column: it resolved to 229px beside a
   368px column of type, floated on 70px of dead paper at each end, and
   read as an accident rather than as an opening.
   ===================================================================== */

.plate {
  margin: 0;
}

.plate img {
  display: block;
  width: 100%;
  /* Required, not optional. The intrinsic `height` attribute is a
     presentational hint that beats everything short of an explicit reset,
     so without this a plate renders at its full pixel height. */
  height: auto;
  border: 1px solid var(--paper-200);
  border-radius: var(--radius-panel);
  box-shadow: var(--shadow-paper);
  background: var(--paper-100);
}

.plate-full img {
  border-radius: var(--radius-case);
}

.plate-inset {
  max-width: 480px;
}

/* .plate-half carries no declarations, by design. Its width is the grid
   column it sits in, which belongs to the page stylesheet — the class
   states which role was chosen so the next reader does not have to infer
   it from the column. Do not delete it as dead CSS, and do not give it a
   width here; a cap would fight the page grid at every breakpoint. */

/* On a graphite ground a paper hairline draws a bright outline around the
   photograph and turns it into a boxed card, which a band may not
   contain. The rule goes graphite and the contact shadow comes off: a
   shadow on a seated surface has nothing to fall onto. */
.on-graphite .plate img {
  border-color: var(--graphite-700);
  box-shadow: none;
}

.plate-caption {
  max-width: 52ch;
  margin-top: var(--space-md);
  color: var(--ink-muted);
  font-family: var(--font-sans);
  font-size: 0.9375rem;
  line-height: 1.5;
}

.on-graphite .plate-caption {
  color: var(--graphite-300);
}

/* =====================================================================
   9. SPEC PANEL (signature)
   A graphite faceplate holding the full technical specification as a
   two-column readout. It always sits BELOW the plain-English lede.
   That vertical order is "plain English first, specs second" expressed
   as layout, and inverting it breaks the core principle.
   ===================================================================== */

.spec-panel {
  background: var(--graphite-900);
  border-radius: var(--radius-panel);
  padding: var(--space-xl) 28px;
  box-shadow: var(--shadow-seated);
}

.spec-panel dl {
  margin: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
}

@media (min-width: 640px) {
  .spec-panel dl {
    grid-template-columns: minmax(0, auto) minmax(0, 1fr);
    column-gap: var(--space-xl);
  }
}

.spec-panel dt,
.spec-panel dd {
  margin: 0;
  padding: var(--space-md) 0;
  border-top: 1px solid var(--graphite-700);
}

.spec-panel dt {
  color: var(--graphite-300);
  font-family: var(--font-sans);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-tracking);
  text-transform: uppercase;
  /* CENTRE THE TEXT, NEVER THE ELEMENT. The key and its value share a grid
     row and both draw the rule that opens the pair, so the rule is a
     division of the row and has to sit on the row's top edge. `align-self:
     center` centred the whole <dt> box and carried its border-top down into
     the middle of the row with it, so the key's rule and the value's rule
     in one pair sat at different heights — by 3px against a one-line value
     and 24px against a three-line one, which read as the panel coming
     apart. Stretching the element keeps the rule where the row is, and the
     flex centring below preserves the optical centring that was wanted. */
  display: flex;
  align-items: center;
}

.spec-panel dd {
  color: var(--paper-200);
  font-family: var(--font-sans);
  font-size: var(--type-data-size);
  font-weight: var(--type-data-weight);
  font-variant-numeric: tabular-nums slashed-zero;
}

/* On a single column the key and its value belong to one row, so the
   value must not repeat the rule that opened the pair. */
@media (max-width: 639px) {
  .spec-panel dd {
    border-top: 0;
    padding-top: 0;
  }
}

/* =====================================================================
   10. READOUT PANEL
   The spec panel's interactive sibling. Where a spec panel holds static
   facts as a <dl>, a readout panel holds a list of ROWS THAT GO
   SOMEWHERE — category counts, quote history, saved drafts. Same
   graphite faceplate, same bevel, same rules.

   Extracted from two independent implementations that had already
   diverged: the landing page's catalog rail and the about page's fleet
   readout were the same object built twice. Any page needing a graphite
   list of linked readings uses this rather than forking it again.

   Scale the figure with --readout-number-size on the panel; everything
   else is fixed.
   ===================================================================== */

.readout-panel {
  --readout-number-size: 1.125rem;
  background: var(--graphite-900);
  border-radius: var(--radius-panel);
  padding: var(--space-xl) 28px var(--space-sm);
  box-shadow: var(--shadow-seated);
}

.readout-panel-head {
  margin: 0 0 var(--space-sm);
  color: var(--graphite-300);
}

.readout-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-lg);
  align-items: center;
  /* Two lines at this padding clear the 44px target floor at every
     viewport without needing a min-height. */
  padding-block: var(--space-lg);
  border-top: 1px solid var(--graphite-700);
  text-decoration: none;
  transition: background-color var(--duration-hover) var(--ease-out-quart);
}

.readout-row:hover {
  background: var(--graphite-800);
  /* Extends the tint to the panel's inner edge so the row reads as a
     full-width target rather than a floating band. */
  box-shadow: -12px 0 0 var(--graphite-800), 12px 0 0 var(--graphite-800);
}

.readout-row-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.readout-row-name {
  color: var(--paper-000);
}

/* Resets the uppercase Label treatment when the name above it is a
   .label, so the gloss reads as a sentence rather than more chrome. */
.readout-row-description {
  color: var(--graphite-300);
  font-family: var(--font-sans);
  font-size: 0.875rem;
  line-height: 1.4;
  letter-spacing: normal;
  text-transform: none;
}

.readout-row-count {
  color: var(--graphite-300);
  white-space: nowrap;
  text-align: right;
}

.readout-row-number {
  color: var(--paper-000);
  font-weight: 700;
  font-size: var(--readout-number-size);
  transition: color var(--duration-hover) var(--ease-out-quart);
}

.readout-row:hover .readout-row-number {
  color: var(--signal-bright);
}

/* Loading, empty, and error copy sits on graphite inside the panel, so
   it takes the lifted text colours rather than the paper ones. */
.readout-panel .catalog-state {
  margin: 0;
  padding-block: var(--space-xl);
  border-top: 1px solid var(--graphite-700);
  color: var(--graphite-300);
  font-family: var(--font-sans);
  font-size: 0.9375rem;
  line-height: var(--type-ui-leading);
}

.readout-panel .catalog-state-error {
  color: oklch(72% 0.15 25);
}

.readout-panel .catalog-state a {
  color: var(--signal-bright);
}

/* =====================================================================
   11. SEATED GRAPHITE BAND
   A graphite object spanning the full content width, for a section that
   should read as equipment rather than as page.

   DESIGN.md §2: graphite is never a page background; it arrives as an
   object placed on the page. This component is what keeps that true at
   full width — it is bounded on all four sides, carries the bevel on its
   top edge and a contact shadow underneath, and requires paper above and
   below it. A graphite region that bleeds to the viewport edges with no
   top or bottom is a dark section, and this system does not have those.

   Never nest a card inside one. Divide its interior with graphite-700
   rules instead.
   ===================================================================== */

.band-graphite {
  background: var(--graphite-900);
  border-radius: var(--radius-case);
  box-shadow: var(--shadow-seated);
  padding: var(--space-3xl) var(--space-xl) var(--space-xl);
  color: var(--paper-200);
}

@media (min-width: 768px) {
  .band-graphite {
    padding: var(--space-4xl) var(--space-3xl) var(--space-2xl);
  }
}

/* =====================================================================
   12. WHAT'S INCLUDED (signature)
   Exists solely to serve "anticipate the incomplete order". Items use a
   graphite check, not a bullet.
   ===================================================================== */

.included {
  background: var(--paper-100);
  border: 1px solid var(--paper-200);
  border-radius: var(--radius-control);
  padding: var(--space-xl);
}

.included ul {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--space-md);
}

.included li {
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr);
  gap: var(--space-md);
  align-items: baseline;
  font-family: var(--font-sans);
  font-size: var(--type-ui-size);
  line-height: var(--type-ui-leading);
}

.included li::before {
  content: '';
  /* A drawn check rather than a glyph, so it renders identically
     everywhere and carries no text semantics for a screen reader. */
  width: 13px;
  height: 8px;
  border-left: 2px solid var(--graphite-700);
  border-bottom: 2px solid var(--graphite-700);
  transform: rotate(-45deg) translate(1px, -2px);
}

/* =====================================================================
   13. RENDERED MARKDOWN
   The paper voice applied to server-rendered markdown: the Details,
   Features, and Includes sections on a product page, and any authored
   long-form content the future portal adds.

   utils/markdown.js emits a small fixed set of elements and escapes its
   input before producing any of them, so this file styles exactly that
   set and never has to defend against arbitrary HTML.

   Promoted from the Inventory/Product round, 2026-07-21. See
   docs/briefs/2026-07-21-inventory-product.md.

   Variants:
     .md-body--graphite   inside a spec panel: the table becomes an
                          engraved two-column readout on the dark faceplate
     .md-body--included   inside the What's Included band: the list takes
                          the drawn check rather than a bullet
   ===================================================================== */

/* =====================================================================
   13.1 PROSE
   Serif explains. The measure cap is what keeps a long Details section
   readable at desktop width.
   ===================================================================== */

.md-body {
  font-family: var(--font-serif);
  font-size: var(--type-body-size);
  line-height: var(--type-body-leading);
  color: var(--ink);
}

.md-body > *:first-child {
  margin-top: 0;
}

.md-body > *:last-child {
  margin-bottom: 0;
}

.md-body p {
  max-width: var(--measure-prose);
  margin: 0 0 var(--space-lg);
  text-wrap: pretty;
}

/* Authored subheadings inside a section. They sit a rank below the
   section's own h2, so they take Title rather than Headline, and they get
   a generous space above so a long Details section reads as several
   passages rather than one wall. */
.md-body h2,
.md-body h3,
.md-body h4 {
  max-width: var(--measure-prose);
  margin: var(--space-2xl) 0 var(--space-md);
  font-family: var(--font-serif);
  font-weight: var(--type-title-weight);
  line-height: var(--type-title-leading);
  letter-spacing: var(--type-title-tracking);
  color: var(--ink);
  text-wrap: balance;
}

.md-body h2 { font-size: var(--type-title-size); }
.md-body h3 { font-size: 1.25rem; }
.md-body h4 { font-size: 1.0625rem; }

.md-body strong {
  font-weight: 700;
}

.md-body em {
  font-style: italic;
}

.md-body a {
  color: var(--signal-ink);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: color var(--duration-hover) var(--ease-out-quart);
}

.md-body a:hover {
  color: var(--graphite-900);
}

/* =====================================================================
   13.2 LISTS
   ===================================================================== */

.md-body ul,
.md-body ol {
  max-width: var(--measure-prose);
  margin: 0 0 var(--space-lg);
  padding-left: var(--space-xl);
  display: grid;
  gap: var(--space-sm);
}

.md-body li {
  padding-left: var(--space-xs);
}

.md-body li::marker {
  color: var(--ink-muted);
}

/* =====================================================================
   13.3 TABLES
   The Features section is authored as a markdown table, so this is the
   spec readout in its paper form: keys quiet on the left, values in the
   sans with tabular figures on the right.

   The renderer wraps every table in a scroll region, so a wide table
   scrolls inside itself rather than pushing the page sideways.
   ===================================================================== */

.md-table-scroll {
  max-width: 100%;
  margin-bottom: var(--space-lg);
  overflow-x: auto;
  /* The wrapper is focusable so the scroll is reachable by keyboard; it
     needs a visible ring like any other focus stop. */
  border-radius: var(--radius-hairline);
}

.md-body table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-sans);
  font-size: var(--type-data-size);
  line-height: 1.5;
}

.md-body thead th {
  padding: var(--space-md) var(--space-lg);
  background: var(--paper-100);
  border-bottom: 1px solid var(--paper-300);
  color: var(--ink-muted);
  font-family: var(--font-sans);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-tracking);
  line-height: var(--type-label-leading);
  text-transform: uppercase;
  text-align: left;
  white-space: nowrap;
}

.md-body tbody th,
.md-body tbody td {
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--paper-200);
  text-align: left;
  vertical-align: baseline;
}

/* The key column. Set quieter than its value so a reader scanning for a
   number finds the number, not the word next to it. */
.md-body tbody th {
  width: 34%;
  min-width: 9rem;
  color: var(--ink-muted);
  font-weight: 400;
}

.md-body tbody td {
  color: var(--ink);
  font-weight: var(--type-data-weight);
  font-variant-numeric: tabular-nums slashed-zero;
  font-feature-settings: 'tnum' 1, 'zero' 1;
}

.md-body tbody tr:last-child th,
.md-body tbody tr:last-child td {
  border-bottom: 0;
}

/* =====================================================================
   13.4 GRAPHITE VARIANT
   Inside a spec panel the same table becomes the engraved readout from
   DESIGN.md section 5: label keys in graphite-300, values in paper-200,
   graphite-700 rules between them, and no header row, because a faceplate
   does not label its own columns "spec" and "value".
   ===================================================================== */

.md-body--graphite {
  color: var(--paper-200);
}

.md-body--graphite p,
.md-body--graphite li {
  font-family: var(--font-sans);
  font-size: var(--type-ui-size);
  line-height: var(--type-ui-leading);
  color: var(--paper-200);
}

.md-body--graphite h2,
.md-body--graphite h3,
.md-body--graphite h4 {
  color: var(--paper-000);
}

.md-body--graphite a {
  color: var(--signal-bright);
}

.md-body--graphite a:hover {
  color: var(--paper-000);
}

.md-body--graphite .md-table-scroll {
  margin-bottom: 0;
}

/* The engraved divisions are the rules between rows. A header row would
   be a fifth piece of chrome on a panel whose whole job is to be read
   quickly, so it is hidden from sight and kept for screen readers. */
.md-body--graphite thead {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.md-body--graphite tbody th,
.md-body--graphite tbody td {
  padding: var(--space-md) 0;
  border-bottom: 0;
  border-top: 1px solid var(--graphite-700);
}

.md-body--graphite tbody tr:first-child th,
.md-body--graphite tbody tr:first-child td {
  border-top: 0;
}

.md-body--graphite tbody th {
  width: 40%;
  padding-right: var(--space-xl);
  color: var(--graphite-300);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-tracking);
  line-height: var(--type-label-leading);
  text-transform: uppercase;
}

.md-body--graphite tbody td {
  color: var(--paper-200);
}

/* =====================================================================
   13.5 INCLUDED VARIANT
   Inside the What's Included band the authored list takes the drawn check
   from base.css section 12 rather than a bullet, and the note that
   usually follows it stays prose.
   ===================================================================== */

.md-body--included ul {
  padding-left: 0;
  list-style: none;
  gap: var(--space-md);
}

.md-body--included li {
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr);
  gap: var(--space-md);
  align-items: baseline;
  padding-left: 0;
  font-family: var(--font-sans);
  font-size: var(--type-ui-size);
  line-height: var(--type-ui-leading);
}

.md-body--included li::before {
  content: '';
  width: 13px;
  height: 8px;
  border-left: 2px solid var(--graphite-700);
  border-bottom: 2px solid var(--graphite-700);
  transform: rotate(-45deg) translate(1px, -2px);
}

.md-body--included p {
  font-size: 1rem;
  color: var(--ink-muted);
}

.md-body--included > p:last-child {
  margin-bottom: 0;
}

/* =====================================================================
   14. REQUEST LIST ROW (signature)
   The cart, renamed. DESIGN.md §5.

   A paper row with a hairline bottom rule: a line on a printed order,
   not a card. Cards are prohibited here for a specific reason — a card
   grid makes each item look like something being sold again, and by this
   point the visitor has already chosen. The row's job is to be checkable
   at a glance, the way you check an order form before signing it.

   NO PRICES ON RENTAL ROWS. Rental pricing is quote-only across the
   entire public site; a used-sale row is the sole exception and carries a
   Data-type figure with tabular numerals.

   The item plate renders only when the item actually has a photograph.
   There is no placeholder graphic anywhere in this system: a grey box
   with an icon in it is a picture of nothing, and a list of them makes a
   real order look broken. Use .request-row--no-plate for the reflow.
   ===================================================================== */

.request-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas:
    'plate identity'
    'actions actions';
  gap: var(--space-lg);
  align-items: start;
  padding: var(--space-xl) 0;
  border-bottom: 1px solid var(--paper-200);
}

.request-row--no-plate {
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas:
    'identity'
    'actions';
}

/* Above 768px the row is one line and the actions sit at its end. */
@media (min-width: 768px) {
  .request-row {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: 'plate identity actions';
    align-items: center;
    gap: var(--space-xl);
  }

  .request-row--no-plate {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: 'identity actions';
  }
}

/* The rule separates rows from each other, so the last one in a list does
   not need one — whatever closes the section provides its own edge. */
.request-rows > .request-row:last-child {
  border-bottom: 0;
}

/* 108x72 is 3:2, which is the ratio every catalog photograph is baked to
   by the admin frame dialog. It was a 72px square with object-fit: cover,
   the one square box on a site of 3:2 sources, and it cropped a third of
   the width off each thumbnail. The grid column is `auto`, so the wider
   plate sizes its own column.

   `contain` is a no-op on a conforming 1200x800 source and is here for the
   one that is not: it renders whole on the paper ground rather than
   cropped by `cover` or stretched by the default `fill`. Both dimensions
   are fixed, so without it a square source is scaled 1.5x horizontally. */
.request-plate {
  grid-area: plate;
  width: 108px;
  height: 72px;
  object-fit: contain;
  background: var(--paper-100);
  border: 1px solid var(--paper-200);
  border-radius: var(--radius-control);
}

.request-row-identity {
  grid-area: identity;
  display: grid;
  gap: var(--space-xs);
  min-width: 0;
}

.request-row-name {
  font-family: var(--font-serif);
  font-size: var(--type-title-size);
  font-weight: var(--type-title-weight);
  line-height: var(--type-title-leading);
  letter-spacing: var(--type-title-tracking);
  color: var(--ink);
  text-decoration: none;
}

a.request-row-name:hover {
  color: var(--signal-ink);
}

/* SKU and eligibility. Set in the sans so it reads as a reference number
   rather than as part of the item's name. */
.request-row-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  color: var(--ink-muted);
  font-family: var(--font-sans);
  font-size: var(--type-data-size);
  line-height: var(--type-data-leading);
  font-variant-numeric: tabular-nums slashed-zero;
}

.request-row-actions {
  grid-area: actions;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-lg);
}

@media (min-width: 768px) {
  .request-row-actions {
    flex-wrap: nowrap;
    justify-content: flex-end;
  }
}

/* Used-sale rows only.
   The width is reserved rather than fitted, so a row carrying a per-unit
   line under its total is the same width as one that is not, and the
   steppers above and below it stay in a column. A list whose controls
   wander horizontally is not scannable. */
.request-row-price {
  min-width: 11ch;
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--type-data-size);
  font-weight: 600;
  line-height: var(--type-data-leading);
  font-variant-numeric: tabular-nums slashed-zero;
  text-align: right;
}

.request-row-price-each {
  display: block;
  color: var(--ink-muted);
  font-weight: 400;
}

/* Removing an item is not the way forward on this page, and it must not
   wear the colour that means "the way forward". Signal blue is reserved
   here for the control that advances the request — with one of these on
   every row, a blue Remove would also put the accent well past the ten
   percent the palette allows. */
.request-row-remove {
  min-height: 44px;
  padding: var(--space-sm) var(--space-md);
  background: transparent;
  color: var(--ink-muted);
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  font-family: var(--font-sans);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-tracking);
  line-height: var(--type-label-leading);
  text-transform: uppercase;
  cursor: pointer;
  transition:
    color var(--duration-hover) var(--ease-out-quart),
    background-color var(--duration-hover) var(--ease-out-quart);
}

.request-row-remove:hover {
  background: var(--paper-100);
  border-color: var(--paper-300);
  color: var(--ink);
}

/* A row whose SKU is no longer in the catalog. The item is named by its
   SKU alone because that is genuinely all that is known about it, and
   inventing a name would be worse than admitting the gap. */
.request-row--orphan .request-row-name {
  color: var(--ink-muted);
}

.request-row-problem {
  color: var(--alert);
  font-family: var(--font-sans);
  font-size: 0.875rem;
  line-height: 1.45;
}

/* ---------------------------------------------------------------------
   14.1 QUANTITY STEPPER
   Two 44px graphite controls flanking a paper readout. Graphite because
   it is a control; the readout between them is paper because it is a
   value being reported, and the system does not blur those two things.

   The glyphs are identified by shape rather than by a word, so each
   control is given a real boundary to satisfy the 3:1 non-text contrast
   rule rather than relying on the glyph alone.
   --------------------------------------------------------------------- */

.stepper {
  display: inline-flex;
  align-items: center;
}

.stepper-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  background: var(--graphite-900);
  color: var(--paper-000);
  border: 1px solid var(--graphite-700);
  font-family: var(--font-sans);
  font-size: 1.125rem;
  line-height: 1;
  cursor: pointer;
  box-shadow: var(--bevel);
  transition:
    background-color var(--duration-hover) var(--ease-out-quart),
    transform var(--duration-press) var(--ease-out-quart);
}

.stepper-btn:first-child {
  border-radius: var(--radius-control) 0 0 var(--radius-control);
}

.stepper-btn:last-child {
  border-radius: 0 var(--radius-control) var(--radius-control) 0;
}

.stepper-btn:hover:not(:disabled) {
  background: var(--graphite-700);
}

.stepper-btn:active:not(:disabled) {
  transform: translateY(1px);
  box-shadow: var(--shadow-pressed);
}

/* Disabled at the floor rather than allowing a decrement to zero that
   would silently remove the item. Removal has its own explicit control. */
.stepper-btn:disabled {
  background: var(--paper-200);
  color: var(--ink-muted);
  border-color: var(--paper-300);
  box-shadow: none;
  cursor: not-allowed;
}

.stepper-value {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 52px;
  height: 44px;
  padding: 0 var(--space-sm);
  background: var(--paper-000);
  color: var(--ink);
  border-block: 1px solid var(--paper-300);
  font-family: var(--font-sans);
  font-size: var(--type-data-size);
  font-weight: 600;
  font-variant-numeric: tabular-nums slashed-zero;
}

/* ---------------------------------------------------------------------
   14.2 INLINE CONFIRMATION CLUSTER
   The system's answer to a destructive action. DESIGN.md §6 prohibits
   native confirm() and alert() outright, so this is the only sanctioned
   pattern for one.

   Two states in one place: an idle control that becomes a prompt with an
   affirmative and a way out. It replaces the trigger in the layout rather
   than appearing beside it, so nothing reflows and the decision happens
   exactly where the visitor was already looking.
   --------------------------------------------------------------------- */

.confirm-cluster {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

.confirm-cluster-prompt {
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--type-data-size);
  line-height: var(--type-data-leading);
}

/* The affirmative is worded, never a bare "Yes" — it names what will
   happen so the sentence survives being read out of context. */
.confirm-yes,
.confirm-no {
  min-height: 44px;
  padding: var(--space-sm) var(--space-md);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  font-family: var(--font-sans);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-tracking);
  line-height: var(--type-label-leading);
  text-transform: uppercase;
  cursor: pointer;
}

.confirm-yes {
  color: var(--alert);
  border-color: var(--alert);
}

.confirm-yes:hover {
  background: var(--paper-100);
}

.confirm-no {
  color: var(--ink-muted);
}

.confirm-no:hover {
  background: var(--paper-100);
  color: var(--ink);
}

.confirm-yes:disabled,
.confirm-no:disabled {
  color: var(--ink-muted);
  border-color: var(--paper-300);
  cursor: not-allowed;
}

/* =====================================================================
   15. NAVIGATION
   A graphite bar: the first piece of equipment on the page. Built against
   DESIGN.md §5 "Navigation".

   The quote link is styled as a control rather than as a destination,
   because it is the single most important action on every public view —
   which is why it alone takes a fill while its neighbours are bare label
   type.

   The markup is public/components/navbar-v2.html and is wired by
   public/js/site-chrome.js, which also owns the mobile menu handler and
   the request count. THAT PARTIAL CARRIES NO COMMENTS, and the rationale
   that would have lived in it is here instead: site-chrome.js injects the
   file with innerHTML, so anything commented in it lands in the live DOM
   on every page of the site and is readable in view-source.
   ===================================================================== */

.site-nav {
  background: var(--graphite-900);
  box-shadow: var(--shadow-seated);
  position: relative;
  z-index: 20;
}

.site-nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xl);
  min-height: 76px;
}

.site-nav-logo {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  border-radius: var(--radius-hairline);
}

/* The bar takes the white-and-color lockup, /images/logo-white-color.png.
   THE PLAIN logo-color.png IS BLACK ON TRANSPARENT and disappears against
   the graphite ground — it fails silently, because the element, its box
   and its alt text are all still correct.

   EVERY LOGO PNG IS NOW TRIMMED TO ITS INK, 395x350. It used to be a
   520x486 canvas that was 24% transparent margin — 55px in from the left,
   66px down from the top — and the consequences were invisible in the
   markup and wrong on screen:

     - a height here drew a mark 28% shorter than the number said, so the
       54px this rule used to carry produced 39px of lockup;
     - the ink began ~6px right of the bar's left edge, so the logo lined
       up with neither the shell gutter nor the page content beneath it.

   39px preserves exactly what 54px used to draw, so the bar is unchanged
   in size and corrected in position. The old comment's reasoning still
   holds and now describes the real number: this is about the floor at
   which the "PRODUCTIONS" wordmark under the monogram reads as type
   rather than as noise, and that floor was always being judged at 39px.

   A height set against these files is now the height of the mark. Size
   any future use of the lockup directly; do not reintroduce a padding
   allowance. */
.site-nav-logo img {
  height: 39px;
  width: auto;
}

.site-nav-menu {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-nav-menu a {
  display: flex;
  align-items: center;
  min-height: 48px;
  padding: var(--space-lg) 14px;
  color: var(--paper-200);
  font-family: var(--font-sans);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-tracking);
  line-height: var(--type-label-leading);
  text-transform: uppercase;
  text-decoration: none;
  /* Reserve the active rule's space at all times so becoming active
     does not shift the row. */
  border-bottom: 2px solid transparent;
  transition: color var(--duration-hover) var(--ease-out-quart);
}

.site-nav-menu a:hover {
  color: var(--paper-000);
}

.site-nav-menu a[aria-current='page'] {
  color: var(--paper-000);
  border-bottom-color: var(--signal-bright);
}

/* The quote link is the one control in the bar, so it reads as a
   control rather than as another destination. */
.site-nav-menu .nav-cta {
  margin-left: var(--space-md);
  padding-inline: var(--space-xl);
  background: var(--signal-ink);
  color: var(--paper-000);
  border-radius: var(--radius-control);
  border-bottom-color: transparent;
  box-shadow: var(--bevel);
  transition:
    background-color var(--duration-hover) var(--ease-out-quart),
    transform var(--duration-press) var(--ease-out-quart);
}

/* How many items are waiting. Reserved from first paint and hidden with
   `visibility` when the list is empty, so the number arriving after the
   cart request resolves cannot push the bar around. Tabular figures keep
   the reserved width honest from 1 to 99. */
/* The running size of the request list, carried on the quote control.
 *
 * IT SHOWS A ZERO RATHER THAN NOTHING, and the markup ships `0` as its
 * initial content. The count is fetched from /api/cart after first paint,
 * so an empty slot filled in later would either reflow the bar or leave a
 * hole in the blue fill; starting at zero means the control is complete
 * from the first frame and the fetch only ever changes the digit.
 *
 * Tabular figures with a slashed zero, min-width 2ch and centred, so the
 * digit does not shift the label as the count changes. KNOWN LIMIT: the
 * control still grows about 3px at ten items, because the Label tracking
 * inherited from .nav-cta is not counted by `ch`. */
.nav-cta-count {
  display: inline-block;
  min-width: 2ch;
  margin-left: var(--space-sm);
  font-variant-numeric: tabular-nums slashed-zero;
  text-align: center;
}

.site-nav-menu .nav-cta:hover {
  background: var(--signal);
}

.site-nav-menu .nav-cta:active {
  transform: translateY(1px);
  box-shadow: var(--shadow-pressed);
}

.site-nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  padding: 0;
  background: var(--graphite-800);
  color: var(--paper-000);
  border: 1px solid var(--graphite-700);
  border-radius: var(--radius-control);
  box-shadow: var(--bevel);
  cursor: pointer;
}

.site-nav-toggle:active {
  transform: translateY(1px);
  box-shadow: var(--shadow-pressed);
}

.site-nav-toggle svg {
  width: 22px;
  height: 22px;
}

/* Mobile: a full-height panel sliding from the right at Lifted
   elevation, on a graphite ground, with 56px link rows. */
@media (max-width: 1023px) {
  .site-nav-toggle {
    display: inline-flex;
  }

  .site-nav-menu {
    position: fixed;
    inset: 0 0 0 auto;
    width: min(84vw, 360px);
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: var(--space-5xl) var(--space-xl) var(--space-xl);
    background: var(--graphite-900);
    box-shadow: var(--shadow-lifted);
    transform: translateX(100%);
    visibility: hidden;
    transition:
      transform var(--duration-panel) var(--ease-out-quint),
      visibility 0s linear var(--duration-panel);
    overflow-y: auto;
  }

  .site-nav-menu.is-open {
    transform: translateX(0);
    visibility: visible;
    transition:
      transform var(--duration-panel) var(--ease-out-quint),
      visibility 0s;
  }

  .site-nav-menu a {
    min-height: 56px;
    border-bottom: 1px solid var(--graphite-700);
  }

  .site-nav-menu a[aria-current='page'] {
    border-bottom-color: var(--signal-bright);
  }

  .site-nav-menu .nav-cta {
    margin-left: 0;
    margin-top: var(--space-xl);
    justify-content: center;
    border-bottom: 0;
  }
}

/* =====================================================================
   16. FOOTER
   ===================================================================== */

.site-footer {
  background: var(--graphite-900);
  color: var(--graphite-300);
  box-shadow: var(--bevel);
  padding-block: var(--space-4xl) var(--space-xl);
  font-family: var(--font-sans);
}

.site-footer-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-3xl) var(--space-xl);
}

.site-footer h2 {
  margin: 0 0 var(--space-lg);
  color: var(--paper-200);
  font-family: var(--font-sans);
  font-size: var(--type-label-size);
  font-weight: var(--type-label-weight);
  letter-spacing: var(--type-label-tracking);
  line-height: var(--type-label-leading);
  text-transform: uppercase;
}

/* The business address. <address> is the right element and it italicizes by
   default, which the sans face does not want, so the style is reset rather
   than the semantics traded away. Sits under the blurb in the first column,
   not in the links list, because it is a fact rather than a control. */
.site-footer-address {
  margin: var(--space-lg) 0 0;
  font-style: normal;
  font-size: var(--type-ui-size);
  line-height: var(--type-ui-leading);
  color: var(--graphite-300);
}

.site-footer ul {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  /* The 44px rows already provide separation; an additional gap would
     make the column read as four detached items rather than a list. */
  gap: 0;
}

/* 44px, not the 32px that footer links usually get. PRODUCT.md commits
   to a 44x44 minimum on every interactive target, and the audience skews
   non-technical and frequently mobile. */
.site-footer a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--paper-200);
  font-size: var(--type-ui-size);
  text-decoration: none;
  transition: color var(--duration-hover) var(--ease-out-quart);
}

.site-footer a:hover {
  color: var(--paper-000);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.site-footer-base {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  margin-top: var(--space-4xl);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--graphite-700);
  font-size: 0.875rem;
}

.site-footer-base p {
  margin: 0;
}

/* =====================================================================
   16. THE ENTRANCE CUE
   Documented in DESIGN.md §4 "Motion Vocabulary". The reference is a
   house lighting cue: a fade time, a stagger between channels, and it
   lands and holds. Nothing here drifts, loops, or follows the scrollbar.

   AUTHORING MODEL — a page wires this with data attributes and writes no
   JavaScript of its own.

     data-cue            A trigger. When it enters the viewport its steps
                         run, once, and it is never observed again.
     data-cue="open"     The page-opening cue. Runs on load from CSS alone
                         with no observer involved, so the statement above
                         the fold does not wait on a script.
     data-cue-step="N"   A channel, numbered 0 through 7 in cue order.

   An element may carry both, which is how a single object reveals on its
   own without a wrapper existing only to trigger it.

   The number is authored rather than counted in JavaScript because the
   open cue starts before any script has run. One mechanism for both cue
   types is worth more than saving eight attribute values.

   §4 "The Cue Rule" governs what earns a channel: objects, not
   paragraphs. A section header is one channel, a photograph is one, a
   list item is one. The page-opening statement is the sole exception.

   Everything is scoped to .js-cue, which public/js/cue.js sets on the
   root element before first paint. A page whose JavaScript fails renders
   every section visible rather than blank, and cue.js declines to set it
   at all under prefers-reduced-motion, which is what makes §17 below
   sufficient without repeating itself here.
   ===================================================================== */

.js-cue [data-cue-step] {
  opacity: 0;
  transform: translateY(var(--cue-rise));
}

/* Channel numbering. Eight is the widest cue on the site (five event
   shapes plus a header, a band, and a plate) and a ninth would mean the
   section wants splitting rather than a longer tail. */
[data-cue-step="0"] { --cue-index: 0; }
[data-cue-step="1"] { --cue-index: 1; }
[data-cue-step="2"] { --cue-index: 2; }
[data-cue-step="3"] { --cue-index: 3; }
[data-cue-step="4"] { --cue-index: 4; }
[data-cue-step="5"] { --cue-index: 5; }
[data-cue-step="6"] { --cue-index: 6; }
[data-cue-step="7"] { --cue-index: 7; }

/* Scroll-triggered cues. Declaring the transition in the same rule that
   restores the resting values is deliberate: the transition arrives with
   the class, so the entrance runs and nothing transitions back out. */
.js-cue [data-cue].is-cued [data-cue-step],
.js-cue [data-cue-step][data-cue].is-cued {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--duration-cue) var(--ease-out-quart),
    transform var(--duration-cue) var(--ease-out-quart);
  transition-delay: calc(var(--cue-index, 0) * var(--cue-stagger));
}

/* The opening cue. Keyframes rather than a class change, so it runs off
   the stylesheet at first paint with no observer and no script timing.

   These channels override the pre-entrance state above and are held back
   by `backwards` fill instead, which applies the from-state through the
   stagger delay and then releases the element entirely. Filling `both`
   would leave every opening element resolved to an identity matrix rather
   than to `none` once it landed, which is a permanent compositing layer
   and a containing block for any descendant, bought for no reason. */
.js-cue [data-cue="open"] [data-cue-step],
.js-cue [data-cue-step][data-cue="open"] {
  opacity: 1;
  transform: none;
  animation: cue-rise var(--duration-cue) var(--ease-out-quart) backwards;
  animation-delay: calc(var(--cue-index, 0) * var(--cue-stagger));
}

@keyframes cue-rise {
  from {
    opacity: 0;
    transform: translateY(var(--cue-rise));
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* =====================================================================
   17. STATE FEEDBACK
   The other half of the motion system, documented in DESIGN.md §4 "The
   Two Registers Rule". An entrance introduces a page; an acknowledgement
   confirms that something the visitor did has landed. Same easing, much
   shorter, no stagger, and it happens every time rather than once.

   Two mechanisms, and the first covers most of the site:

   1. ANYTHING REVEALED BY REMOVING [hidden] ANIMATES ITSELF, with no
      script involved. `[hidden]` is `display: none`, so an animation
      declared on the element cannot run until the attribute comes off.
      Alerts, confirmations, empty states and error panels are all
      reached that way. See DESIGN.md §4 "The Arrival Rule".

   2. CONTENT REPLACED IN PLACE has no CSS hook, so it is announced from
      script: public/js/feedback.js toggles .is-announcing and the
      keyframe below replays. Used for the nav request count, the
      confirmation cluster, the catalog result set, and product pricing.

   Reduced motion needs nothing extra here — §18 collapses every
   animation-duration on the page, so all of this becomes instant.
   ===================================================================== */

@keyframes state-in {
  from {
    opacity: 0;
    transform: translateY(var(--state-rise));
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Mechanism 2. The class is added after the content is swapped and
   removed on animationend, so the keyframe can replay on the next
   change; a class left on the element would never fire twice. */
.is-announcing {
  animation: state-in var(--duration-state) var(--ease-out-quart);
}

/* Mechanism 1, for the one component of this kind that lives in base:
   the form alert from DESIGN.md §5. Page-level panels reached the same
   way — the product confirmation, the catalog empty and error states —
   declare this in their own stylesheet against the same keyframe.

   Scoped to :not([hidden]) rather than the bare class so the animation
   is attached only in the state that can actually display it. */
.form-alert:not([hidden]) {
  animation: state-in var(--duration-state) var(--ease-out-quart);
}

/* =====================================================================
   18. REDUCED MOTION
   Transitions collapse to opacity only. No essential information is
   conveyed by motion.
   ===================================================================== */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  /* The mobile panel still needs to arrive and leave, so it fades in
     place rather than sliding. */
  .site-nav-menu {
    transition: opacity 0.01ms !important;
  }

  /* cue.js declines to set .js-cue when this preference is on at load,
     so normally nothing above is ever hidden. This cancels the
     pre-entrance state for the case where the preference is switched on
     mid-session, when the class is already on the root element and the
     content would otherwise be left sitting at zero opacity. */
  .js-cue [data-cue-step] {
    opacity: 1;
    transform: none;
    animation: none !important;
  }
}

/* =====================================================================
   19. UTILITIES
   ===================================================================== */

.visually-hidden {
  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;
}

/* Locks background scroll while the mobile menu panel is open. */
.scroll-locked {
  overflow: hidden;
}
