/* static/overrides.css — loaded after pico.min.css.
 *
 * Single source of design tokens (RESEARCH.md Pitfall 4 / UI-SPEC): every
 * color, spacing, and typography value used across templates lives here as
 * a CSS custom property — no hex literals or ad-hoc px values in .templ
 * files (D-09).
 *
 * Pico v2 scopes its own light-mode variable values under
 * `:root:not([data-theme=dark])`, specificity (0,2,0) — higher than a
 * plain `:root` (0,1,0), so a plain `:root` override would silently lose
 * the cascade to Pico's own values regardless of `<link>` order. The
 * Pico-variable overrides below mirror that exact selector so ours
 * actually wins. Our own brand-only custom properties (--sp-*, --space-*,
 * --text-*) don't shadow any Pico variable name, so they're declared in
 * plain :root blocks.
 *
 * Dark mode (prefers-color-scheme) is intentionally NOT supported — the
 * site renders ONE dark brand palette at all times (Phase 9, D-01) and
 * still never branches on the visitor's OS `prefers-color-scheme` setting.
 * This is a deliberate product decision, not an oversight: earlier in this
 * project a light palette shipped as the one-and-only theme; Phase 9
 * promoted Phase 7's checkout-scoped dark "heat press" palette to the
 * whole site instead, but the "exactly one palette, no OS branching" rule
 * itself is unchanged.
 *
 * A CSS comment ends at the FIRST close sequence it contains, not the last
 * — so no comment in this file may ever use a wildcard shorthand that puts
 * an asterisk directly against a forward slash (e.g. "max-*" immediately
 * followed by "/auto"), because that silently truncates the comment early
 * and dumps its leftover prose into the stylesheet as a bogus rule
 * prelude, which discards the very next rule entirely. This cost this
 * project two silently-dead rules (.design-thumb and .swatch) before it
 * was caught; static/overrides_css_test.go now fails the build if a
 * comment in this file ever closes early again.
 */

:root {
  /* Spacing scale (4px multiples) */
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 32px;
  --space-2xl: 48px;
  --space-3xl: 64px;

  /* Typography: exactly 4 sizes, 2 weights (matches Pico's own 400/700) */
  --text-body-size: 16px;
  --text-body-weight: 400;
  --text-body-line-height: 1.5;

  --text-label-size: 14px;
  --text-label-weight: 400;
  --text-label-line-height: 1.5;

  --text-heading-size: 24px;
  --text-heading-weight: 700;
  --text-heading-line-height: 1.2;

  --text-display-size: 32px;
  --text-display-weight: 700;
  --text-display-line-height: 1.2;

  /* Brand tokens (Phase 9, D-01): promoted site-wide from Phase 7's
     .checkout-theme-scoped values, which are Phase 7's own already-WCAG-
     verified literals, not BRAND-INTAKE.md's literal numbers. --sp-heat-
     mid-a/--sp-heat-mid-b are two additional scroll-heat interpolation
     stops, both actively consumed by the scroll backdrop's three-stop
     void -> heat-mid-a -> heat-mid-b path (09-08 checkpoint, matched to
     an external reference document's exact "Page background" table) --
     see the body rule below and TestHeatBackdropPerceptualRange. This
     superseded an interim two-stop void->(heat-mid-b blended with
     --sp-ember, then --sp-amber) ramp from Plan 09-07/09-08's earlier
     iterations, and before that 09-07-PLAN.md's own note that a
     three-stop ramp was "unverifiable in this environment" -- it turned
     out to be verifiable once the actual root cause (a CSS stacking bug
     making the backdrop invisible regardless of its color math) was
     found and fixed. Both locked BRAND-INTAKE.md hex values are
     unchanged throughout all of this. */
  --sp-void: #100D12;
  --sp-cold-purple: #3B1E5A;
  --sp-ember: #E0402F;
  --sp-amber: #FFB020;
  --sp-cream: #FFF1D6;
  /* Text-opacity scale (09-08 checkpoint follow-up, matched to an
     external reference document): body text stays full --sp-cream
     (unchanged); --sp-cream-secondary (60%) and --sp-cream-muted (45%)
     are two new tiers for secondary/de-emphasized text and muted
     labels respectively. 45% is the reference document's own stated
     floor ("never go lighter -- it fails contrast on the warm end of
     the scroll"); not independently re-verified against every backdrop
     state here, but the current three-stop backdrop path
     (void/heat-mid-a/heat-mid-b, all near-black) is darker than
     whatever backdrop that floor was computed against, so it errs
     conservative rather than optimistic.

     Currently unconsumed: this pass's one application (.nav-eyebrow)
     was itself removed later in the same session at the user's request
     (the distinct eyebrow treatment looked visually disconnected from
     its sibling nav items). Left declared as ready-to-use infrastructure
     for whenever a real secondary/muted text instance needs it -- a
     full site-wide text-opacity audit is separate, larger scope this
     checkpoint never attempted. */
  --sp-cream-secondary: rgba(255, 241, 214, 0.60);
  --sp-cream-muted: rgba(255, 241, 214, 0.45);
  --sp-card: rgba(255, 241, 214, 0.045);
  /* Corrected from BRAND-INTAKE's literal 14% opacity, which computes to
     only 1.42:1 against the void background — far under the 3:1 WCAG
     1.4.11 non-text-contrast minimum required for form-control
     boundaries. 40% verified at 3.53:1 against void and 3.24:1 against
     the card surface (the tighter of the two constraints) — do not lower
     this value; this rationale was proven correct in Phase 7 (checkout)
     and now applies to every form control on every page. */
  --sp-border: rgba(255, 241, 214, 0.40);
  --sp-heat-mid-a: #241019;
  --sp-heat-mid-b: #2D130E;

  /* Accent — now the brand amber (Phase 9, D-01): repointed from the old
     light-theme terracotta literal to the brand palette so every existing
     --sp-accent consumer (wordmark underline, current-page nav underline,
     the .warning left-border) picks up the dark-theme brand color with no
     template edit. */
  --sp-accent: var(--sp-amber);
  /* Destructive — reserved for the admin "Delete photo" control only.
     Kept as a semantic danger red, independent of the brand palette — a
     deliberate, documented exception to BRAND-INTAKE.md's "these five
     only" palette rule, scoped to the admin delete control alone. */
  --sp-destructive: #c0392b;
}

/* Dark mode (prefers-color-scheme) is intentionally NOT supported — the
   site renders ONE dark brand palette at all times (Phase 9, D-01) and
   never branches on the visitor's OS setting. Deliberate product decision,
   unchanged from Phase 1: only the one palette itself is now the promoted
   brand-dark theme instead of the original light theme. */

/* Brand theme: void background + cream text, ember/amber interactive
   colors (D-09/D-01; brand-dark palette). Mirrors Pico's own light
   selector's specificity — see header comment: a bare :root here would
   silently lose the cascade to Pico's own light-mode defaults regardless
   of <link> order, since templates/layout.templ renders
   data-theme="light". --pico-primary/--pico-primary-background are
   deliberately NOT the void token — see 09-01-PLAN.md's
   <planner_corrections> point 2: that would render every link and every
   button void-on-void. Amber-on-void is 10.55:1 for link text; void-on-
   ember is 4.55:1 for button text (bold weight is the safety margin, see
   the .checkout-btn-primary-derived primary-button rule below). */
:root:not([data-theme=dark]) {
  --pico-background-color: var(--sp-void);
  --pico-color: var(--sp-cream);
  --pico-h1-color: var(--sp-cream);
  --pico-h2-color: var(--sp-cream);
  --pico-h3-color: var(--sp-cream);
  --pico-h4-color: var(--sp-cream);
  --pico-h5-color: var(--sp-cream);
  --pico-h6-color: var(--sp-cream);
  --pico-muted-color: var(--sp-cream);
  --pico-muted-border-color: var(--sp-border);
  --pico-card-background-color: var(--sp-card);
  --pico-card-border-color: var(--sp-border);
  --pico-form-element-background-color: var(--sp-card);
  --pico-form-element-border-color: var(--sp-border);
  --pico-form-element-color: var(--sp-cream);
  --pico-form-element-placeholder-color: var(--sp-cream);
  --pico-form-element-active-background-color: var(--sp-card);
  --pico-form-element-active-border-color: var(--sp-amber);
  --pico-form-element-focus-color: var(--sp-amber);

  --pico-primary: var(--sp-amber);
  --pico-primary-hover: var(--sp-amber);
  --pico-primary-background: var(--sp-ember);
  --pico-primary-hover-background: var(--sp-ember);
  --pico-primary-inverse: var(--sp-void);
}


/* Typography roles mapped onto the elements templates actually use.

   UAT gap closure (09-08 checkpoint follow-up): background-color was a
   flat var(--sp-void) here, with the actual scroll-driven interpolation
   living on a separate .heat-backdrop div (position:fixed; z-index:-1;
   first child of body). That div was never visible in a real browser --
   a position:fixed element with a NEGATIVE z-index and no stacking
   context on any ancestor renders behind the canvas paint that body's
   own background-color propagates to (CSS2.1 14.2), not in front of it,
   in every engine tested. The chip and coil (both z-index:55/56, i.e.
   ABOVE the header) worked correctly the whole time -- only the backdrop
   itself was silently painting behind everything, which is why 09-07's
   contrast-range fix measured correctly in tests but never once appeared
   on screen. Moving the interpolation onto body's own background-color
   sidesteps the whole stacking question by construction: body's
   background IS what canvas propagation uses, so there is no longer a
   separate element that can end up behind it. Any page whose Layout()
   call passes heat=false stays flat void exactly as before, since
   var(--heat-t, 0) falls back to 0 and heat.js is never loaded there
   (originally every non-home page, Phase 9 D-02; narrowed post-ship to
   just the admin surface -- see templates/layout.templ). */
body {
  font-size: var(--text-body-size);
  font-weight: var(--text-body-weight);
  line-height: var(--text-body-line-height);
  color: var(--sp-cream);
  /* Piecewise progress through the three-stop background path below,
     derived once here (on body, so --heat-t's only writer) and inherited
     by every --heat-t reader including .design-card -- custom properties
     inherit down the DOM regardless of paint position. --heat-seg1 goes
     0->1 across the FIRST half of the scroll (--heat-t 0->0.5) and holds
     at 1 for the second half; --heat-seg2 stays 0 for the first half and
     goes 0->1 across the second (--heat-t 0.5->1). Chaining two
     color-mixes keyed to these two segments produces a real three-stop
     linear interpolation (void -> mid-a -> mid-b) from the single 0..1
     --heat-t value color-mix() alone can't express, since color-mix()
     only ever blends two colors. No conditional logic needed: min()/
     max() alone make each segment naturally clamp to its own half. */
  --heat-seg1: min(1, var(--heat-t, 0) * 2);
  --heat-seg2: max(0, calc((var(--heat-t, 0) - 0.5) * 2));
  /* Plain fallback: if the engine cannot parse the nested color-mix
     below, this declaration stands and body renders flat void --
     degraded, never transparent. This ordering is load-bearing; do not
     reorder or remove either declaration.

     Three-stop path (09-08 checkpoint follow-up): matches an external
     reference document's exact "Page background" table verbatim --
     #100D12 top / #241019 middle / #2D130E footer -- which are,
     byte-for-byte, this project's own already-locked --sp-void/--sp-
     heat-mid-a/--sp-heat-mid-b tokens (no new hex literal, C-4).
     Replaces three earlier same-session attempts that all blended in a
     bright accent (--sp-ember, then --sp-cold-purple, then --sp-amber)
     chasing a contrast-ratio floor sized for the background to carry
     the mechanic's noticeability on its own. The reference document
     names the background "the subtlest, since it's behind everything"
     -- the coil and chip (their own already-vivid amber/ember/cold-
     purple and cream/amber ramps, unchanged by this) are what's meant
     to read as obviously "temperature rising", not this. Deliberately
     reintroduces void->mid-b's original measured 1.11:1 contrast
     (the literal figure UAT test 2 reported as imperceptible) as the
     FOOTER extreme, on the strength of this being confirmed the
     intended design rather than an oversight -- see
     TestHeatBackdropPerceptualRange for the resulting threshold
     change and why it's deliberate here, not a silently reopened gap. */
  background-color: var(--sp-void);
  background-color: color-mix(in srgb,
    color-mix(in srgb, var(--sp-void) calc((1 - var(--heat-seg1, 0)) * 100%), var(--sp-heat-mid-a) calc(var(--heat-seg1, 0) * 100%))
      calc((1 - var(--heat-seg2, 0)) * 100%),
    var(--sp-heat-mid-b) calc(var(--heat-seg2, 0) * 100%));
}

/* Grain overlay (Phase 9, D-01; BRAND-INTAKE.md): a fixed, full-viewport
   fractal-noise texture over every page, below the lightbox (z-index 1
   vs. .lightbox's 100). pointer-events: none is what keeps this from ever
   swallowing a click -- without it a full-viewport fixed layer would sit
   on top of every interactive element on the page. The data URI below is
   an author-written compile-time constant (no request value is ever
   interpolated into it); its angle brackets and "#" fragment reference
   are percent-encoded, and it contains no "*" immediately followed by "/"
   anywhere, so it cannot close this file's CSS comments early -- see the
   file header comment and TestOverridesCSSCommentsCloseWhereAuthored,
   which fails the build if that ever recurs.

   Parameters matched to an external reference document (09-08 checkpoint
   follow-up): baseFrequency 0.8 (was 0.9), numOctaves 4 (was 2), tile
   160x160 (was 200x200, set via the SVG's own width/height -- no
   separate background-size needed since the tile repeats at its natural
   intrinsic size by default), opacity 0.13 (was 0.12).
   TestHeatBackdropPerceptualRange's grain-bounded readability assertion
   reads this opacity back from the stylesheet rather than hardcoding it,
   so it automatically tracks this change. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 1;
  opacity: 0.13;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='sp-grain'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23sp-grain)'/%3E%3C/svg%3E");
}

/* Headings (Phase 9, D-01): Anton, uppercase, tight tracking, promoted
   site-wide from Phase 7's checkout-scoped h1/h2,h3 rules. Every font
   stack in this file ends in a system fallback so a failed font fetch
   degrades legibly rather than breaking layout. */
h1 {
  font-family: 'Anton', Impact, sans-serif;
  font-size: 32px;
  line-height: 1.2;
  text-transform: uppercase;
}

h2,
h3 {
  font-family: 'Anton', Impact, sans-serif;
  font-size: 20px;
  line-height: 1.2;
  text-transform: uppercase;
}

/* Display — the wordmark (D-08) and the homepage hero headline only; a
   higher-specificity class selector so it wins over the h1/h2 rule above
   for the one h1 that should render at Display size. */
.wordmark,
.hero-headline {
  font-family: 'Anton', Impact, sans-serif;
  font-size: var(--text-display-size);
  font-weight: var(--text-display-weight);
  line-height: var(--text-display-line-height);
}

.wordmark {
  /* .wordmark/.hero-headline share Anton sizing above; h1 gets its
     uppercase transform from the h1 tag rule, but this is a <span>, so
     it needs its own text-transform to match. */
  text-transform: uppercase;
  border-bottom: 3px solid var(--sp-accent);
  padding-bottom: var(--space-xs);
}

/* Header nav only gives the wordmark's <ul> narrow available width once
   the nav-links <ul> also needs room, so the full 32px Display size
   wrapped "SP Print Vision" across 3 lines on a phone, and individual nav
   labels like "Log In"/"Sign Up" broke mid-phrase across lines too,
   pushing real page content far below the fold (live UAT, 2026-08-30).
   Fix stacks the header into two clean rows on narrow phones instead of
   fighting to cram both <ul>s onto one: the wordmark's <ul> takes the
   full row width (so "SP Print Vision" fits on one line even at a
   readable size), the nav-links <ul> wraps onto its own row below with a
   visible gap, and nowrap on every nav label stops any single link's own
   text from splitting across lines -- desktop/tablet keep the original
   single-row layout and full Display size untouched. */
@media (max-width: 480px) {
  header nav {
    flex-wrap: wrap;
  }
  header nav > ul:first-of-type {
    flex: 1 0 100%;
    margin-bottom: var(--space-md);
  }
  header nav a,
  header nav button {
    white-space: nowrap;
  }
  .wordmark {
    font-size: 24px;
  }
}

/* Wordmark's degree mark (BRAND-INTAKE.md: "the degree mark is part of
   the name, superscript, always in ember red") — fixed ember, never on
   the heat ramp, distinct from --sp-accent (amber) which drives the
   wordmark's underline. */
.wordmark-degree {
  color: var(--sp-ember);
}

/* Body copy and form controls (Phase 9, D-01): Space Grotesk, promoted
   site-wide from Phase 7's checkout-scoped body/label/input/select/
   textarea rule. Form controls and buttons need explicit listing here
   (rather than relying on inheritance from body) because <input>,
   <select>, <textarea>, and <button> do not inherit font-family from an
   ancestor by default in any browser. */
body,
label,
input,
select,
textarea,
button,
[role="button"] {
  font-family: 'Space Grotesk', system-ui, sans-serif;
}

nav a {
  font-size: var(--text-label-size);
  font-weight: var(--text-label-weight);
  line-height: var(--text-label-line-height);
}

/* Header account menu (2026-09-11, live UAT, revised twice same day): a
   click-to-reveal icon consolidating everything session-specific that used
   to sit as separate nav items -- the logged-in identity, the My
   Orders/Admin link, and Log Out. First pass made identity click-to-reveal
   instead of permanent header text; second pass ("they should have the
   admin and then the logout button there instead of just having it
   separately") folded the link and Log Out into the same dropdown, so
   clicking the icon is now the one place all of it lives.
   templates/layout.templ's accountMenu always renders the link and Log Out
   (a session predating the identity stamp must not lose its only way to
   reach My Orders/Admin or to log out), but only shows the "Logged in as"
   line when identity is known -- so there's no empty, layout-shifting
   element for that stale-session case.

   The trigger (<summary>) is sized/colored exactly like its `nav a`
   siblings -- not a distinct icon-only treatment -- for the same reason
   the Policies note below gives: a visually disconnected nav element has
   been tried and reverted here twice already. Native <details>/<summary>
   needs its own marker hidden (browsers draw a default disclosure
   triangle) since the icon itself is the affordance. */
.nav-account {
  position: relative;
}

/* Pico styles <details>/<summary> as a document-style accordion by
   default: both elements carry their own margin-bottom: var(--pico-spacing)
   (1rem, stacking to ~32px total), and summary::after paints Pico's own
   chevron glyph (rotating on [open]) alongside the native marker Pico
   already hides. Left alone, that made this nav item noticeably taller
   than its Home/About Us/Policies siblings -- the flex row's
   align-items: center then centered it on its own inflated height,
   visually detaching the icon from the text baseline beside it ("looks
   like it's hanging", 2026-09-11 live UAT). All of it reset to zero here:
   this control is a compact nav trigger, not an accordion, and the
   person-icon SVG is already the disclosure affordance, so Pico's own
   chevron is redundant on top of it. */
.nav-account details,
.nav-account summary {
  margin-bottom: 0;
}
/* Pico's own details[open] > summary{margin-bottom:...} (specificity
   0,1,2) would otherwise beat the plain .nav-account summary reset above
   (0,1,1) the instant the menu is opened, silently reintroducing the gap
   only while expanded -- .nav-account's class stacked onto the same
   [open] > summary shape (0,2,2) closes that gap. */
.nav-account details[open] > summary {
  margin-bottom: 0;
}

.nav-account summary {
  list-style: none;
  cursor: pointer;
  color: var(--sp-cream);
}
.nav-account summary::-webkit-details-marker,
.nav-account summary::marker,
.nav-account summary::after {
  display: none;
}

/* Panel styling borrows .status-pill's border/spacing convention, but NOT
   its background: --sp-card is a ~4.5%-opacity tint meant for a card that
   sits inline on the page's own solid background, not a floating overlay
   -- at that opacity the page content underneath (on an admin page,
   sitting right below the header, that's often the Status filter select)
   showed through and visually collided with this panel's own text ("make
   sure the text in the box is aligned well", 2026-09-11 live UAT, though
   the real defect was translucency, not alignment). --sp-void is the
   site's actual solid background color, so the panel now reads as a fully
   opaque surface the way every other floating overlay in this codebase
   (e.g. .lightbox) does.

   Right-aligned under the icon so it can never overflow past the header's
   right edge, the direction every other nav item already reads toward.
   Stacked column layout: identity line (if any), then the section link,
   then Log Out -- each full-width so the whole row is clickable, not just
   the text. */
.nav-account-panel {
  position: absolute;
  top: 100%;
  right: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-xs);
  margin-top: var(--space-xs);
  padding: var(--space-xs);
  min-width: 160px;
  max-width: min(280px, calc(100vw - 2 * var(--space-md)));
  background-color: var(--sp-void);
  border: 1px solid var(--sp-border);
  overflow-wrap: anywhere;
}

/* Explicit padding here, not just inherited from the shared nav-a/button
   touch-target rule, because Pico gives <button> its OWN, much larger
   horizontal padding by default (button{--pico-form-element-spacing-
   horizontal:2rem}, i.e. 32px, versus this row's 16px) at the same
   (0,0,1) specificity as that shared rule's bare `button` selector --
   equal specificity is supposed to fall to source order (this file loads
   after pico.min.css) but proved unreliable in practice here, so this
   selector is written to unambiguously outrank Pico's on specificity
   alone (0,2,1) rather than depend on source-order tie-breaking. */
.nav-account-panel a,
.nav-account-panel button[type="submit"] {
  justify-content: flex-start;
  width: 100%;
  padding: var(--space-sm) var(--space-md);
}

/* Root cause of Admin/My Orders (the <a>) still reading as indented
   relative to Log Out and the identity line, found by actually rendering
   the page and measuring computed styles (2026-09-11 live UAT, third
   attempt at this fix -- the padding rule above was correct but
   incomplete): Pico gives every nav link a NEGATIVE margin sized to
   exactly cancel its OWN default padding (nav li :where(a,[role=link]){
   margin: calc(...) * -1; padding: calc(...); }) -- a standard technique
   for enlarging the click target without visually shifting the text. But
   that negative margin is sized to PICO's own default padding value, not
   the --space-md padding this rule declares above, so the two don't
   cancel out and the link renders ~9.5px left of where its padding alone
   would put it. Nothing above touched `margin` on the <a>, so Pico's
   was the only declaration in effect. */
.nav-account-panel a {
  margin: 0;
}

.nav-account-panel form {
  margin: 0;
}

.nav-account-identity {
  margin: 0;
  padding: var(--space-xs) var(--space-md);
  font-size: var(--text-label-size);
  color: var(--sp-cream-secondary);
}

/* Policies nav treatment (Phase 9, D-01; 09-UI-SPEC.md §7) removed
   09-08 checkpoint follow-up: the distinct eyebrow-style treatment
   (uppercase, bold, letter-spaced, muted) made "Policies" look visually
   disconnected from Home/Our Work/My Orders next to it -- the user
   called it out as looking like it didn't belong. Policies is now a
   plain `nav a` like every other nav item. */

/* Real form-field labels use the Body role (16px/400), NOT the 11px
   eyebrow/label-role size (Phase 9, D-01) — a deliberate narrowing carried
   forward from Phase 7's checkout-scoped rule: 09-UI-SPEC.md's literal
   "labels are 11px" applies to eyebrow/micro-labels only (see
   .sp-eyebrow/.checkout-eyebrow below), never to a real form-field label,
   since 11px labels on any conversion-critical form would be a real
   legibility regression. */
label {
  font-size: var(--text-body-size);
  font-weight: var(--text-body-weight);
  line-height: var(--text-body-line-height);
}

/* Label role also applies to buttons (D-09 Typography Contract) —
   previously scoped to nav a only; label was split out above into its own
   Body-role rule (Phase 9, D-01). */
button, [role="button"], input[type="submit"] {
  font-size: var(--text-label-size);
  font-weight: var(--text-label-weight);
  line-height: var(--text-label-line-height);
}

/* Header/footer band: the elements themselves (not just their inner
   .container) carry the void background so the band spans the full
   viewport width, while the inner .container keeps content aligned with
   the rest of the page. Set directly rather than through the Pico
   interactive-color family (Phase 9 <planner_corrections> point 2) --
   --pico-primary-inverse is now void, so reading it here would render
   void-on-void. */
header,
footer {
  background-color: var(--sp-void);
  color: var(--sp-cream);
}

header a,
footer a {
  color: var(--sp-cream);
}

/* Sticky brand chrome (Phase 9, D-01; BRAND-INTAKE PAGE ORDER item 1): the
   header stays pinned while the page scrolls. A separate header-only rule
   from the header/footer band rule above, since only the header (not the
   footer) is sticky. z-index stays below .lightbox's 100 so the lightbox
   overlay still covers the header.

   Blur-on-scroll (09-08 checkpoint follow-up, matched to an external
   reference document): overrides the shared header/footer rule's solid
   void background with an 85%-opacity one (within the reference's
   documented 75-90% range) plus a 12px backdrop blur, so content
   scrolling underneath softens rather than hard-cuts at the header's
   edge -- footer is untouched, it was never sticky so nothing scrolls
   under it. -webkit- prefix included since Safari has historically
   required it for backdrop-filter; the unprefixed declaration comes
   last so a browser supporting both uses the standard one. If neither
   is supported the header simply stays solid void (today's look),
   never transparent -- there's no way to detect backdrop-filter support
   in plain CSS, but the visual failure mode of an unsupported
   backdrop-filter is "no blur", not "no background", so this degrades
   safely with no fallback declaration needed. */
header {
  position: sticky;
  top: 0;
  z-index: 50;
  background-color: color-mix(in srgb, var(--sp-void) 85%, transparent);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
}

/* Header account-menu "Log Out" control (Phase 2; folded into the
   account-menu dropdown 2026-09-11): a CSRF-protected POST form, styled to
   match the plain nav items around it rather than Pico's default filled
   button (which would otherwise render void-on-void against the header
   band). appearance: none strips the browser's native button chrome --
   without it, Safari/Chrome keep a small intrinsic inset that isn't fully
   overridable by the CSS padding alone, which was throwing Log Out's text
   out of alignment with the plain <a> and <p> above it in the dropdown
   ("not aligned in the same line", 2026-09-11 live UAT). Underline
   removed the same round -- the owner wants it reading as a plain menu
   row like Admin/My Orders above it, not singled out as a link. */
header nav form button[type="submit"] {
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: none;
  color: var(--sp-cream);
  font-size: var(--text-label-size);
  font-weight: var(--text-label-weight);
}

header .container,
footer .container {
  padding-top: var(--space-sm);
  padding-bottom: var(--space-sm);
}

/* Footer two-column layout (2026-08-25 live UAT): the nav-links line on
   the left, the new contact-info block (layout.templ's Layout doc
   comment) on the right. wrap + a floor gap so the two blocks never
   collide as the viewport narrows; pushed hard right above the 480px
   breakpoint every other mobile rule in this file uses -- narrower than
   that, .footer-contact switches to left-aligned/no-auto-margin (below)
   so it stacks under the links line exactly like a normal paragraph
   rather than awkwardly right-hanging on a narrow screen.

   .footer-contact's own margin-left: auto (not text-align: right on
   .footer-grid's justify-content: space-between alone -- 2026-08-25 live
   UAT follow-up, "move it more right") is the actual fix: a flex item
   shrinks to its own content width by default, so text-align: right on
   the item has nothing to push against -- there's no leftover space
   inside a box that's already exactly as wide as its text. margin-left:
   auto is the standard flex technique that works regardless: it claims
   all remaining space on the item's OWN flex line, pushing the whole box
   (not just the text inside it) to the line's right edge, in both the
   single-line desktop case (redundant with space-between there, but
   harmless) and the wrapped/stacked case (where space-between and
   text-align: right alone would silently do nothing). */
.footer-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-sm) var(--space-lg);
}

.footer-contact {
  margin-left: auto;
  text-align: right;
}

@media (max-width: 480px) {
  /* Live UAT follow-up, 2026-08-30: owner wants the contact line kept
     right-aligned and on one row even on a phone, rather than the
     previous left-aligned/stacked fallback -- overrides that fallback.
     The full phone+email+address text is long enough that fitting it on
     one line needs a smaller size than the desktop default; nowrap stops
     the browser's own line-wrap from breaking it up awkwardly instead. */
  .footer-contact {
    margin-left: auto;
    text-align: right;
    white-space: nowrap;
    font-size: 11px;
  }
}

/* Header felt oversized at Pico's default nav-item padding (1rem
   top+bottom per <li>, on top of the .container padding above) — tighten
   it for the header specifically; footer has no logo so it doesn't need
   the same treatment. */
header nav {
  --pico-nav-element-spacing-vertical: 0.5rem;
}

/* Touch targets: >=44px for nav links, buttons, and the admin
   upload/delete controls (WCAG 2.5.5, STORE-03) — via padding, never by
   overriding the spacing scale itself (44 = 4 x 11, still a 4px multiple).
   summary added 2026-09-11 for the header account-disclosure trigger —
   same rule, same reason, it's just as clickable as a nav a or button. */
nav a,
button,
[role="button"],
input[type="submit"],
summary {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: var(--space-sm) var(--space-md);
}

/* WhatsApp contact button (Phase 8, WHATSAPP-01) -- a fixed, always-visible
   floating bubble rather than a header-nav link (revised post-UAT: a
   support/contact CTA needs to stay reachable while scrolled deep into a
   page, which a header link cannot do). Pinned bottom-LEFT, not
   bottom-right, deliberately -- the right edge is already occupied by
   Phase 9's fixed-position heat-coil (12px wide, spans the full viewport
   height) and its readout chip; sharing that edge risks visual/touch-
   target collision, especially at narrow phone widths where both would
   compete for the same corner. z-index 60 sits above the header (50) and
   the heat-coil/readout (55) so the bubble is never occluded, but below
   the lightbox (100) so a design-preview lightbox still wins if ever
   opened over it. Fill color is WhatsApp's own brand green (#25D366),
   a deliberate, explicit one-element exception to BRAND-INTAKE's 5-token
   palette (user's own call, post-UAT) -- the whole point of this element
   is instant recognition as "the WhatsApp button", which the site's own
   amber/ember/cold-purple palette cannot signal on its own. White icon on
   this green is the same pairing WhatsApp's own app/brand assets use;
   note its WCAG contrast ratio (~2:1) sits below the general 3:1
   non-text-UI floor this project holds everywhere else -- accepted here
   specifically because brand recognizability is the point of this single
   icon-only element, not a precedent for relaxing contrast elsewhere. */
.whatsapp-bubble {
  position: fixed;
  left: var(--space-lg);
  bottom: var(--space-lg);
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background-color: #25D366;
  color: #FFFFFF;
  line-height: 1;
  text-decoration: none;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
  transition: transform 200ms ease;
}

.whatsapp-bubble:hover {
  transform: scale(1.06);
}

@media (prefers-reduced-motion: reduce) {
  .whatsapp-bubble {
    transition: none;
  }

  .whatsapp-bubble:hover {
    transform: none;
  }
}

/* Bottom padding cut well below the top (live UAT, 2026-08-25): the
   original symmetric var(--space-3xl) on both sides stacked with Pico's
   own section{margin-bottom} rule to leave a large, purposeless void
   between the "Design Your Own" button and the catalog search box right
   after it -- the top padding still needs the room to breathe under the
   nav, the bottom does not, since the button itself already reads as a
   clear break before the next section. */
/* Full-width hero banner (live UAT, 2026-08-27; briefly replaced with a
   full-bleed-on-desktop-too treatment on 2026-08-30, then reverted the
   same day once the owner saw the resulting top/bottom cropping on a
   wide screen and asked to go back to this original approach). Earlier
   pass stretched the video via object-fit: cover to fill this entire box
   edge to edge -- across a ~1700px-wide viewport that's roughly 3x the
   source footage's native 576px width, which is exactly the visible
   softness that got flagged as "clarity is down". Source resolution is a
   hard ceiling: fill-the-whole-width, stay sharp, and stay uncropped
   can't all be true at once for a 576px-wide clip in a fluid-width
   section, so this keeps the video at its own near-native size (see
   .hero-media) anchored to the right, with the surrounding box just
   staying plain background rather than stretching anything to fill it. */
.hero {
  position: relative;
  height: 480px;
  margin-top: var(--space-3xl);
  margin-bottom: var(--space-lg);
  border-radius: 16px;
  overflow: hidden;
  background-color: var(--sp-void);
  box-shadow: 0 24px 48px rgba(0, 0, 0, 0.55);
}

/* Phone follow-up (2026-08-30): the source clip's 3:2 shape (576x384)
   doesn't match a phone's narrow, tall viewport, so a fixed height forces
   object-fit: cover to crop the video's left/right edges to fill it --
   the "frame cutting off" the owner flagged. Matching .hero's own box
   shape to the clip's real aspect ratio via aspect-ratio (instead of a
   fixed px height) means object-fit has nothing left to crop: the box is
   already exactly the clip's shape, so the whole frame shows uncropped.
   Concretely this makes the hero notably SHORTER on a phone (~250-285px
   at typical widths) than the previous fixed 420px, not taller --
   showing the complete frame of a wide/short clip on a narrow screen
   necessarily produces a shorter box, the opposite of what "grow to fit
   the whole frame" sounds like it should do. Also closes the gap the
   owner flagged between the sticky header and the hero -- margin-top
   drops from --space-3xl to --space-sm here, matching every other
   max-width: 992px mobile rule's breakpoint for this component. */
@media (max-width: 992px) {
  .hero {
    height: auto;
    aspect-ratio: 3 / 2;
    margin-top: var(--space-sm);
  }
}

/* Anchored right at near-native size with a left-edge fade into .hero's
   matching --sp-void background, restored 2026-08-30 (see .hero's own
   comment above for the full revert history). Mobile keeps its own
   full-bleed override below -- the phone-specific aspect-ratio fix this
   same day means .hero's own box already matches the clip's shape there,
   so object-fit: cover on a full-width/height video has nothing left to
   crop, unlike the desktop case this treatment exists to avoid. */
.hero-media {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: auto;
  z-index: 0;
  -webkit-mask-image: linear-gradient(to right, transparent 0, black 140px);
  mask-image: linear-gradient(to right, transparent 0, black 140px);
}

@media (max-width: 992px) {
  .hero-media {
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    -webkit-mask-image: none;
    mask-image: none;
  }
}

/* Absolutely positioned with explicit inset (not position: relative +
   height: 100%, live UAT, 2026-08-30 follow-up): a percentage height only
   resolves against a parent with a definite height, and Safari has known
   quirks resolving that against a parent sized via aspect-ratio (as
   .hero now is on mobile, replacing its old fixed px height the same
   day) rather than an explicit height value -- height: 100% silently
   collapsed to this element's own content size there, so justify-content
   had no extra space to push the text/button toward and they landed near
   the top of the tall mobile box instead of the intended bottom. inset: 0
   fills the parent directly with no percentage-height dependency, so it
   works the same regardless of how .hero's own height was established. */
.hero-copy {
  position: absolute;
  inset: 0;
  z-index: 2;
  max-width: 560px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  padding: 0 var(--space-2xl);
}

/* Hero intro line (live UAT, 2026-08-25): previously plain, unstyled body
   text at the same size and weight as everything else on the page -- it
   sat under the bold Anton headline without reading as a tagline at all.
   Sized between body and heading, in the softer secondary cream tone
   (the same hierarchy trick --sp-cream-secondary already provides
   elsewhere) so it reads as a supporting line, not competing with the
   headline above it or the button below it. max-width keeps the line
   length comfortable on a wide viewport instead of stretching edge to
   edge. */
.hero-subtext {
  font-size: 24px;
  font-weight: 500;
  line-height: 1.35;
  color: var(--sp-cream-secondary);
  max-width: 46ch;
  margin-bottom: var(--space-lg);
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.85), 0 0 24px rgba(0, 0, 0, 0.6);
}

/* Mobile copy placement + color (live UAT, 2026-08-30 follow-up) -- MUST
   stay AFTER both base rules above, not before: .hero-copy/.hero-subtext
   here share identical specificity with their base-rule counterparts
   (one class selector each), and when specificity ties, plain CSS source
   order decides the winner regardless of which side is inside an
   @media block -- a later unconditional rule beats an earlier matching
   media-query rule. This exact block originally sat BEFORE the base
   rules and was silently overridden by them on every mobile load despite
   the media query correctly matching -- confirmed against a real
   screenshot showing centered placement and washed-out color that no
   amount of further tuning fixed, because the rules moved here were
   never actually winning the cascade in the first place. Don't move this
   block back above .hero-copy/.hero-subtext's base rules.

   The video covers the full section width on mobile (unlike the anchored
   desktop treatment above, where text sits over plain background, never
   actual video pixels), so the centered placement and 60%-opacity
   secondary cream read as sitting in the middle of the footage and
   blending inconsistently against whatever's moving behind them --
   flagged as "really distracting". Pushed to the bottom of the box
   instead of centered, and bumped to solid white with a stronger shadow
   for reliable contrast regardless of what's playing underneath. Desktop
   is intentionally untouched -- text there was never over video content
   in the first place, so this problem never applied to it.

   Solid white + text-shadow alone still wasn't reliably readable against
   the clip's own brighter/white-fabric frames -- shadow only darkens
   right at each glyph's edge, not the area behind it, so a bright enough
   frame still shows through between letters/strokes. Adds a
   bottom-anchored dark gradient BEHIND the text -- transparent at the
   top of .hero-copy, fully opaque only in the bottom third where the
   text/button actually sit -- a narrower, more targeted version of the
   2026-08-27 "no scrim" decision: that one rejected darkening the whole
   video, this only darkens the fraction of it directly behind the copy. */
@media (max-width: 992px) {
  .hero-copy {
    justify-content: flex-end;
    /* max-width: none overrides the base rule's 560px cap (2026-08-30
       verification follow-up): the base value exists to keep desktop
       line-length readable, but on mobile .hero-copy also carries the
       scrim background below -- capped at 560px, that background left
       everything past it (up to the video's full width, 992px) with no
       scrim at all on tablet-ish widths between 560-992px, the exact gap
       a second-session review caught. */
    max-width: none;
    padding: 0 var(--space-md) var(--space-lg);
    background: linear-gradient(to top, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0.75) 30%, transparent 70%);
  }

  .hero-subtext {
    font-size: 16px;
    color: #ffffff;
    text-shadow: 0 2px 14px rgba(0, 0, 0, 0.95), 0 0 32px rgba(0, 0, 0, 0.85);
  }
}

/* Hover/focus treatment for a filled ember button (Phase 9, D-01): an
   underline was a light-theme leftover appropriate to a plain-text CTA,
   but is wrong for a filled control -- brightness dip reads as "pressed"
   on any solid fill, matching button.contrast's own hover treatment
   below. Covers every primary-button consumer, not just the hero CTA. */
.checkout-btn-primary:hover,
.checkout-btn-primary:focus,
.sp-btn-primary:hover,
.sp-btn-primary:focus,
.hero-cta:hover,
.hero-cta:focus {
  filter: brightness(0.92);
}

/* Destructive control — the admin "Delete photo" button, the one
   destructive action in Phase 1 (D-04); admin/photos.templ already marks
   it with Pico's .contrast class, so no admin template edit is needed. */
button.contrast {
  background-color: var(--sp-destructive);
  border-color: var(--sp-destructive);
  color: #ffffff;
}

button.contrast:hover,
button.contrast:focus {
  background-color: var(--sp-destructive);
  border-color: var(--sp-destructive);
  filter: brightness(0.9);
}

/* Admin sub-nav (Phase 2): pipe-separated text nav rendered directly under
   each admin page's <h1> — Orders | Prices | Designs (Log out moved into
   the header's account menu 2026-09-11, see that section's comment).
   Label-weight throughout.

   justify-content history, all 2026-09-11 live UAT, same session:
   1) <nav> elements get justify-content: space-between from Pico by
      default (its assumed logo-left/links-right shape), which this bare
      class selector never overrode -- with only 3 items left after Log
      out's removal, that stretched Orders/Designs into the row's literal
      left/right corners ("aligned to the right most corner").
   2) justify-content: center (+ a bumped-up gap) fixed the corner-pinning
      but, on a wide admin viewport, rendered as a small tight cluster
      marooned in the middle of a mostly-empty row ("orders on the left
      looks really empty, designs on the right looks really empty").
   3) space-evenly is the actual fix for both complaints at once: it uses
      the row's FULL available width (matching the Status filter/table
      below it, so the row doesn't read as empty on either side) while
      guaranteeing equal space before Orders and after Designs too --
      unlike space-between, neither end item ever touches the row's edge. */
.admin-subnav {
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  /* A floor, not the real spacing -- space-evenly computes far more than
     this from the row's actual full width on desktop. Kept small so it
     doesn't force overflow/wrapping on a narrow phone, where space-evenly
     has much less width to distribute. */
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
  font-size: var(--text-label-size);
  font-weight: var(--text-label-weight);
  /* Delimited-band treatment (Plan 09-04, D-01): mirrors the site header's
     own nav conventions so the sub-nav reads as a bounded strip under the
     page <h1>, not a loose row of links. */
  border-bottom: 1px solid var(--sp-border);
  padding-bottom: var(--space-sm);
}

/* Sub-nav link color (Plan 09-04, D-01): explicit cream, rather than
   inheriting the amber link color -- amber here would make the sub-nav
   read louder than the page heading it sits under. */
.admin-subnav a {
  color: var(--sp-cream);
}

/* Responsive gallery grid (admin /admin/photos, the only remaining consumer
   as of quick task 260810-mdq, which removed the public /gallery page this
   class used to also serve) — reflows to a single column on narrow
   viewports (STORE-03); object-fit keeps photo aspect ratios consistent
   across source images. */
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-xl);
}

.gallery-grid img {
  width: 100%;
  height: 220px;
  object-fit: cover;
}

@media (max-width: 480px) {
  .gallery-grid {
    grid-template-columns: 1fr;
  }
}

/* Standard visually-hidden-but-accessible utility (live-UAT round, 260903):
   removes an element from sighted view without removing it from the
   accessibility tree or tab order -- the well-known "clip to 1px" pattern
   (display:none/visibility:hidden would hide it from screen readers too,
   which defeats the point). First use: PreviewFragment's split-layout
   confirmation line, which duplicates its two sibling figcaptions visually
   but still needs to reach assistive tech per D-04. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Non-blocking design-upload resolution warning (D-07, Phase 2): distinct
   from a hard .error — accent-orange left border, never red, and never
   attached to any control that disables "Place Order". */
.warning {
  border-left: 3px solid var(--sp-accent);
  padding: var(--space-xs) var(--space-sm);
}

/* Admin order-detail status-advance buttons (each its own <form
   style="display:inline-block"> for independent POSTs) — without an
   explicit gap the inline-block forms sit flush against each other with
   no visible seam, reading as one merged control instead of distinct
   buttons (Phase 3 checkpoint feedback). */
.status-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

.status-actions form {
  margin: 0;
}

/* Admin order-detail photo galleries (Phase 4: DESIGN-01/FINAL-01) — reuses
   .gallery-grid; each figure's inline delete form only needs a small top
   margin to separate it from the caption/image above. */
.order-photo-figure form {
  margin-top: var(--space-xs);
}

/* Customer thumbnail-grid CSS :target lightbox (Phase 4, D-05) — the first
   real "extra interactivity" in this codebase, implemented with ZERO
   JavaScript per RESEARCH.md Pattern 5: each thumbnail links to
   #photo-<gate>-<n>; the matching .lightbox overlay is hidden by default
   and shown via the :target pseudo-class. The close control links to "#_"
   (never a bare "#", which scrolls to top instead of closing). */
.lightbox {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.85);
  z-index: 100;
}

.lightbox:target {
  display: flex;
  align-items: center;
  justify-content: center;
}

.lightbox img {
  max-width: 90vw;
  max-height: 90vh;
}

/* :target has no Escape-key close without JS — mitigated with a clearly
   visible, adequately-sized (>=44px) close control per RESEARCH.md's own
   accessibility-tradeoff note. */
.lightbox-close {
  position: absolute;
  top: var(--space-md);
  right: var(--space-md);
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-heading-size);
  color: #ffffff;
  background: rgba(0, 0, 0, 0.5);
  border-radius: 50%;
  text-decoration: none;
}

/* Missing-catalog-photo placeholder (D-02, Phase 6): sized to drop into the
   same grid slot .gallery-grid img occupies so the grid's layout math is
   unaffected by which cards have real photos vs. placeholders. Dashed
   border + muted tint make it unmistakable for a real photo at a glance —
   distinct from the solid-bordered gallery figure. No new color tokens, no
   accent fill (UI-SPEC Color section — accent stays reserved for
   underline/hover treatments only). */
.design-placeholder {
  width: 100%;
  height: 220px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  border: 2px dashed var(--pico-muted-border-color);
  background: var(--pico-card-background-color);
  color: var(--pico-muted-color);
}

.design-placeholder svg {
  width: 48px;
  height: 48px;
}

/* Catalog detail view (CATALOG-03): a larger photo than the grid card's
   compact 220px slot, reusing the existing spacing token for the max width
   rather than a new px literal. margin-left/right: auto (260807, UAT
   finding) centers the photo inside .configurator-preview-col, which is
   usually wider than the photo's own 480px cap -- without this the photo
   sits flush against the column's left edge instead of in the middle of
   it. */
.catalog-detail-photo {
  width: 100%;
  max-width: 480px;
  height: auto;
  display: block;
  margin-bottom: var(--space-md);
  margin-left: auto;
  margin-right: auto;
}

.catalog-detail-placeholder {
  max-width: 480px;
  height: 320px;
  margin-bottom: var(--space-md);
  margin-left: auto;
  margin-right: auto;
}

/* Catalog detail two-photo swipe view (260806-jdk, D-05/D-06): when a
   design has both a front and a back photo, CatalogDetailPage renders this
   container in place of the single .catalog-detail-photo img above. Pure
   CSS scroll-snap -- no JavaScript is added anywhere for this; native
   horizontal overflow scroll plus mandatory snap gives drag/arrow-key
   swiping for free. Deliberately no scroll-behavior: smooth -- this
   project's motion is gated behind prefers-reduced-motion, and native
   scroll-snap needs no animation to feel responsive. max-width reuses
   .catalog-detail-photo's own 480px so the swipe view occupies the exact
   same column the single-photo layout does. margin-left/right: auto
   (260807, UAT finding) matches .catalog-detail-photo's own centering
   inside .configurator-preview-col. */
.catalog-photo-swipe {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  gap: var(--space-md);
  max-width: 480px;
  margin-bottom: var(--space-md);
  margin-left: auto;
  margin-right: auto;
}

/* Each panel is a full-width, non-shrinking flex item so exactly one photo
   sits at each snap position regardless of viewport width, with a
   Front/Back figcaption label rather than a dot indicator (D-05: a label
   announces which face is showing to a screen reader, a dot only says
   "there is more"). No default figure margin, so the swipe container's own
   gap is the only spacing between panels. */
.catalog-photo-panel {
  flex: 0 0 100%;
  scroll-snap-align: start;
  margin: 0;
}

.catalog-photo-panel figcaption {
  font-size: var(--text-label-size);
  color: var(--sp-cream);
  text-align: center;
  padding-top: var(--space-xs);
}

/* Price prominence + position (260831, second round of the same feedback:
   the first attempt -- a giant standalone Anton number under the photo --
   read as too big and disconnected from what it was pricing, and lost the
   shipping breakdown entirely). Checked against a real product page's
   structure (Allbirds' PDP) for the convention: title, then price, then the
   size picker -- price sized and weighted to stand out from body text, but
   not hero-sized, and the itemized detail (what's included) stays visible
   right there with it, not stripped down to a bare number.
   #checkout-line-items becomes a flex column purely so .checkout-price-block
   (order: -1) can render first, ahead of the size/quantity row(s) and "Add
   Another Size" -- CheckoutLineItemsFragment's own markup order is
   untouched (see that templ's doc comment), this is presentation only. The
   card treatment (border/background) reuses the exact --sp-border/--sp-card
   pair .status-pill already established above, rather than inventing a new
   "boxed" language. */
#checkout-line-items {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

/* Sticky (260831, third round: customer feedback that "Add Another Size"
   sits below the price reads as backwards -- adding a row is the thing
   that changes the price, so the customer has to scroll back UP to see the
   result of what they just did). Confirmed against the standard ecommerce
   order-summary pattern (sticky total while the line-item list grows) --
   see ecommerceguide.com/patterns/order-summary. Mirrors this codebase's
   own existing sticky element, header's rule above (position: sticky, a
   near-opaque blurred background so scrolled-under rows don't show through
   messily) rather than inventing a second sticky convention -- z-index 10
   stays well under header's 50 so the header always wins if the two ever
   overlap. Sticks relative to #checkout-line-items/.configurator-controls-col
   (neither sets overflow, so the normal document scroll applies), so it
   naturally stops sticking once that column scrolls out of view -- a
   fourth-round attempt replaced this with a viewport-fixed bottom bar to
   remove that limit entirely, but the owner tried both and preferred this
   sticky-while-in-column behavior, especially now that the size chart
   renders ahead of this card (catalog.templ) rather than right before
   Continue, leaving more room before the column runs out.

   top offset (260831, live-mobile bug fix): header is ALSO position:
   sticky/top:0, and at the >480px width this file uses everywhere else as
   its one mobile breakpoint, header nav wraps into two rows (wordmark row
   + nav-links row, see the header nav media query above) instead of one --
   noticeably taller there than on desktop/tablet. A single small offset
   was sized for neither case: too small a gap under the ~60px single-row
   desktop header, and far too small under the ~130px two-row mobile
   header -- so once both this card and the header were stuck at the same
   time, the header (z-index 50, opaque-ish) painted over this card
   (z-index 10) instead of sitting cleanly above it. Both values below are
   generous estimates from the header's own CSS (padding + line-height +
   the two-row gap on mobile), not measured in a real browser -- flagging
   that explicitly since this needs a visual check on an actual phone.

   Split in two (260903, /custom-upload live-UAT round): .checkout-price-block
   itself now carries the card look (border/background/blur/padding)
   UNCONDITIONALLY, so any page using this class gets the same highlighted-box
   treatment -- /custom-upload's own tier-price display (templates/customer/
   custom_upload.templ) was rendering with none of this because it isn't a
   descendant of #checkout-line-items, just the bare .checkout-numeric
   tabular-figure font. The sticky/top/z-index positioning first stayed
   scoped to #checkout-line-items only, on the theory /custom-upload's
   layout "was never designed around it" -- reverted later the same round
   (owner: "can the price also scroll like how it does for the catalog
   designs") once it was clear the sticky behaviour itself, not just the
   card look, was the actual ask. `order: -1` stays #checkout-line-items-only
   (it only makes sense inside that flex list of per-size/quantity rows,
   which /custom-upload has no equivalent of), but sticky/top/z-index now
   also apply under .configurator-controls-col directly -- the paragraph
   above this comment already named .configurator-controls-col alongside
   #checkout-line-items as a valid sticky-relative-to container (catalog's
   own detail page nests the latter inside the former; /custom-upload has
   only the former), so this isn't a new mechanic, just finishing wiring an
   already-anticipated second case. */
.checkout-price-block {
  border: 1px solid var(--sp-border);
  background-color: color-mix(in srgb, var(--sp-void) 88%, transparent);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  padding: var(--space-sm) var(--space-md);
  /* Live-UAT catch (260903): a bare <div> has no default vertical margin
     the way a <p>/<select>/<label> does, so without this the card's own
     border sat flush against whatever followed it (on /custom-upload,
     @checkoutfields.TierExplainer()'s <details> right below) -- invisible
     before this became a bordered card, glaring once it was one. On the
     checkout page this is additive to #checkout-line-items's own
     `gap: var(--space-md)` between flex children (harmless -- gap and
     margin both simply add space, they don't fight), so one rule here
     covers every context this class is used in without a second,
     page-scoped override. */
  margin-bottom: var(--space-md);
}

#checkout-line-items .checkout-price-block {
  order: -1;
}

#checkout-line-items .checkout-price-block,
.configurator-controls-col .checkout-price-block {
  position: sticky;
  top: 80px;
  z-index: 10;
}

@media (max-width: 480px) {
  #checkout-line-items .checkout-price-block,
  .configurator-controls-col .checkout-price-block {
    top: 160px;
  }
}

/* The itemized lines (per-size, shipping) stay plain body text -- only the
   Total line, the one number a customer actually needs at a glance, gets
   the size/weight/color bump. 1.125rem (260831 third-round feedback: the
   original 1.375rem still read as too large) -- a deliberately modest step
   up from --text-body-size's 16px, not a second heading. */
.checkout-price-block .checkout-total {
  font-size: 1.125rem;
  font-weight: bold;
  color: var(--sp-amber);
}

/* Boxed treatment for the D-13 back-design/tier mismatch notice (live-UAT
   round, 260903: "I think we should be more clear about the warning...
   maybe a similar box"). The shared .warning class (plain left-border,
   no background) is used in three OTHER templates -- an admin section, an
   order-history table cell, and a generic customer <p> -- in contexts this
   heavier treatment was never designed for, so this is scoped to the
   notice's own stable #sublimation-mismatch-notice id rather than
   restyling .warning everywhere. Mirrors .checkout-price-block's own
   border/background/padding almost exactly -- the same calm, neutral
   void/border card language as the price box right above it on this same
   page, not a red/error box (D-13 explicitly forbids that treatment) --
   plus .warning's existing left-border accent kept inside so it still
   reads as a distinct NOTICE, not a second price line.
   :has(.warning) gates the box itself: this div always renders, even with
   nothing to say (customUploadTierMismatchNotice's own doc comment --
   an out-of-band swap always needs a target), so an unconditional border/
   background here would show an empty boxed card the rest of the time. */
#sublimation-mismatch-notice:has(.warning) {
  border: 1px solid var(--sp-border);
  background-color: color-mix(in srgb, var(--sp-void) 88%, transparent);
  padding: var(--space-sm) var(--space-md);
  margin-bottom: var(--space-md);
}

#sublimation-mismatch-notice .warning {
  margin: 0;
  padding-left: var(--space-sm);
}

/* Identical boxed treatment for the tier coverage notice (owner feedback,
   260903, after first seeing it as a bare paragraph: "make it like the
   user will not miss [it]") -- the exact same complaint this file's
   #sublimation-mismatch-notice rule above already solved once, so this
   reuses that recipe rather than inventing a second "prominent but calm"
   language. Scoped to this element's own id for the same reason the
   mismatch notice's rule is scoped rather than restyling .warning
   everywhere, and gated on :has(.warning) for the same reason: this div
   always renders, even empty (blank tier, no selection yet), so an
   unconditional border/background would show an empty boxed card most of
   the time. See customUploadTierCoverageNotice's own doc comment
   (templates/customer/custom_upload.templ) for why reusing .warning's
   accent-orange border is a deliberate choice here, not an error/red
   treatment. */
#sublimation-coverage-notice:has(.warning) {
  border: 1px solid var(--sp-border);
  background-color: color-mix(in srgb, var(--sp-void) 88%, transparent);
  padding: var(--space-sm) var(--space-md);
  margin-bottom: var(--space-md);
}

#sublimation-coverage-notice .warning {
  margin: 0;
  padding-left: var(--space-sm);
}

/* Catalog detail two-column layout (260807, second UAT round): the first
   fix here centered the page's content in a single narrow column
   (.catalog-detail-band, since removed) inside .checkout-theme's own
   cap-and-center convention -- correct as far as it went, but a single
   ~640px column still reads as mostly-empty black space on a wide
   viewport once you're looking at a whole page of it, not just a button.
   Reworked to reuse the SAME two-column grid the garment configurator
   already established (.configurator-body/-preview-col/-controls-col,
   Phase 6.1) instead of inventing a second layout system: photo on the
   left, name/material/size/quantity/price/buttons on the right, collapsing
   to one column at .configurator-body's existing 480px breakpoint. No new
   grid CSS needed here -- CatalogDetailPage just adopts the existing
   classes; see templates/storefront/catalog.templ. */

/* Garment configurator (Phase 6.1, 06.1-03): the home-page two-column
   layout (preview pane left, selection controls right on wide viewports),
   collapsing to a single column at the same narrow-viewport breakpoint
   .gallery-grid already established (UI-SPEC: "no new breakpoint token,
   reuse the existing... media-query pattern"). Selected-state treatment
   (tabs/swatches/toggle/flip) reuses --pico-primary/--pico-primary-inverse
   throughout -- never --sp-accent, which stays reserved for underline/hover
   treatments only (UI-SPEC Color section). */
.configurator-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-xl);
  margin-top: var(--space-2xl);
}

@media (max-width: 480px) {
  .configurator-body {
    grid-template-columns: 1fr;
  }
}

.preview-pane {
  background-color: var(--pico-background-color);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-lg);
}

.preview-pane img {
  max-width: 100%;
  height: auto;
}

/* Split preview layout (06.1-21, PREVIEW-03): when a design is staged for
   the shown view, #preview-pane renders two <figure>s instead of one <img>.
   Browsers give <figure> a default margin that would fight .preview-pane's
   own flex gap above, so it's zeroed here and replaced with the pane's own
   --space-sm rhythm between each figure's image and its caption. */
.preview-figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
}

.preview-figure figcaption {
  color: var(--pico-muted-color);
  font-size: var(--text-label-size);
  text-align: center;
  max-width: 44ch;
}

/* The placement guide is deliberately smaller than the customer's own
   as-uploaded artwork so it reads as clearly secondary -- 60% is the single
   tunable number if the end-of-phase visual review wants the guide larger
   or smaller. */
.preview-placement img {
  max-width: 60%;
}

/* Deliberate NON-change: .preview-as-uploaded img intentionally inherits
   ONLY the .preview-pane img cap above (max-width: 100%; height: auto) and
   must NOT be given width: 100%, since that would upscale a small upload
   and blur the exact artwork this split exists to show faithfully. */

.tab-group,
.swatch-group,
.segmented-toggle,
.flip-control {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
}

.flip-control {
  margin-bottom: 0;
}

/* Repaired (Phase 9, D-01): Task 1 repointed --pico-primary to amber and
   --pico-primary-inverse to void, which would leave these controls
   readable but wrong (an amber-bordered ghost button reads as a warning
   state, not a neutral unselected one). This rule keeps the three
   remaining siblings (.tab-button, .toggle-button, .flip-button) visible
   and byte-identical to before. (The garment-color swatch button variant
   that used to be split out here was removed along with the garment-color
   picker -- see custom_upload.templ.) */
.tab-button,
.toggle-button,
.flip-button {
  border: 1px solid var(--sp-border);
  background: transparent;
  color: var(--sp-cream);
}

.tab-button-selected,
.toggle-button-selected,
.flip-button-selected {
  background-color: var(--sp-ember);
  border-color: var(--sp-ember);
  color: var(--sp-void);
}

/* Field-heading-label role (2026-08-25 live UAT, round 3): round 2's
   match to the color-swatch label's 11px, bold, uppercase, letter-spaced
   treatment turned out too small once actually applied --
   it reads fine as a tiny caption under a 44px swatch circle, but as a
   standalone label sitting above a full-width dropdown or upload button
   it looked disproportionately tiny. The owner then asked directly what
   font/size the "GARMENT" <h2>
   uses (Anton, 20px, uppercase, tight tracking -- see the h2/h3 rule
   below) and asked for that SAME heading treatment instead, on Material,
   Size Chart's <summary>, Text Color, and Font -- these four were the
   ones still at the full 20px size as of this round (Font's own label
   was later removed entirely along with its select, a later owner
   correction -- see the Font-select-removed comment in
   templates/customer/custom_upload.templ; Sublimation Coverage joined
   this same size after this round too, Phase 12). Round 4: Front Design, Front
   Text, Name, and Jersey Number moved to .field-heading-label-small
   instead (below) -- "reduce the size a little bit" -- and Logo/Back
   Design joined that same smaller class rather than staying plain,
   completing the full Design-section label set. Special Instructions
   joined the same smaller class in round 5 -- see
   .field-heading-label-small's own comment below for that history.
   Phase 13 live-UAT (2026-09-05): Front Text moved back to this full-size
   class, undoing its round-4 downsize -- owner asked for it to match
   Text Color's size once it landed on page 2 next to Text Color/Name/
   Jersey Number. Name and Jersey Number joined it moments later in the
   same live-UAT round ("make the name and the jersey number bigger") --
   all four of page 2's own labels (Front Text, Text Color, Name, Jersey
   Number) are now this full size. Front Design stays at the smaller size
   below; it lives on page 1 and was not part of this ask. */
.field-heading-label {
  font-family: 'Anton', Impact, sans-serif;
  font-size: 20px;
  line-height: 1.2;
  text-transform: uppercase;
}

/* field-heading-label-small (round 4, 2026-08-25 live UAT): the SAME
   Anton/uppercase/tight-tracking treatment as .field-heading-label above,
   one step down in size -- "we changed the size for the front design,
   front text, name and jersey number a little bit [too big]... reduce
   the size a little bit", applied to those four plus Logo and Back
   Design (previously plain, unstyled labels -- brought into the same
   family rather than left out, matching this page's overall
   move-toward-consistency arc). 16px sits between round 2's 11px eyebrow
   attempt (too small) and the 20px heading size reserved for
   Material/Size Chart/Text Color/Font (the "you must choose this, it
   affects price/looks" tier) -- a visibly smaller but still clearly
   Anton/bold/uppercase secondary tier, not a return to a plain label.
   Special Instructions also joined this class in round 5 ("the special
   instruction thing also changed the font") -- round 2's deliberate
   Space Grotesk exception (a lighter weight than Anton allows) is gone;
   the owner asked for the font itself to match too, not just the size.
   Front Text, Name, and Jersey Number all left this class in the Phase 13
   live-UAT round noted above (.field-heading-label's own comment) --
   only Front Design (page 1) remains here of the original round-4 group. */
.field-heading-label-small {
  font-family: 'Anton', Impact, sans-serif;
  font-size: 16px;
  line-height: 1.2;
  text-transform: uppercase;
}

/* Text-color control (D-09, 09-05; simplified 2026-08-07): the native
   colour picker and its hex readout, laid out as a single flex row.
   Replaces the old five-button .swatch-group row for this one field only
   -- the text-shape control keeps that row unchanged. Originally also
   carried a second, separate preview swatch showing the same color a
   second time; removed on direct UAT feedback ("why do we have two colors
   like that? they're not aligned well") once the <input> itself (below)
   was restyled to already look like a clean swatch, making the second
   circle pure redundancy rather than a fallback for anything the input
   couldn't show on its own. */
.text-color-control {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
}

/* The native colour input's own dark-theme footprint (restyled 2026-08-07,
   UAT feedback: the browser-default square swatch read as "big... doesn't
   match the theme" next to the pill-shaped Font/Straight-Curved controls
   below it). The outer box stays 44px square -- the site-wide touch-target
   floor, unchanged from before -- but appearance:none plus the
   pseudo-element resets below shrink the VISIBLE colour circle to 36px,
   centered inside that 44px click target (this achieves the same
   proportions the only way available for a single self-contained input
   rather than a button wrapping a span). Verified in Chromium's rendering
   model (::-webkit-color-swatch-wrapper/::-webkit-color-swatch);
   Firefox's ::-moz-color-swatch has no separate wrapper part, so its
   padding is set directly on the pseudo-element instead -- both converge
   on the same visible ~36px circle, but this has not been cross-browser
   screenshot-verified beyond Chromium. */
input[type="color"] {
  width: 44px;
  height: 44px;
  padding: 0;
  background-color: transparent;
  border: none;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  /* UAT feedback (2026-08-07): the circle sat visibly high against the
     Text Color label/hex text next to it. align-items:center on the
     flex row centers this input's own box against the text's line-box,
     but the native input's intrinsic rendering doesn't sit dead-center
     within that box the way a plain block element would -- this small
     nudge is an empirical correction for that mismatch, not a value
     derived from a formula. Iterated live against user feedback: 4px,
     7px, 9px, settled at 10px. */
  margin-top: 10px;
}

input[type="color"]::-webkit-color-swatch-wrapper {
  padding: var(--space-xs);
}

input[type="color"]::-webkit-color-swatch {
  border: 1px solid var(--sp-border);
  border-radius: 50%;
}

input[type="color"]::-moz-color-swatch {
  padding: var(--space-xs);
  border: 1px solid var(--sp-border);
  border-radius: 50%;
}

/* Hex readout (D-09, 09-UI-SPEC.md Copywriting Contract: "rendered as the
   literal uppercase hex value"). Mirrors the eyebrow/micro-label role's
   typographic values (not shared via selector list with .sp-eyebrow, same
   deliberate-duplication precedent as thead th above) plus
   font-variant-numeric: tabular-nums so the hex digits don't jitter as the
   customer drags the picker. */
.text-color-hex-label {
  font-family: 'Space Grotesk', system-ui, sans-serif;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.4;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--sp-cream);
  font-variant-numeric: tabular-nums;
}

/* Per-slot design confirmation thumbnail + circular Remove control
   (round-8 gaps 2/3, 06.1-33): supersedes the square icon-only Remove
   button 06.1-29 shipped (the rule that used to live here), on direct
   customer instruction -- "I also want to see what I uploaded in a small
   preview box beside the select your file... I just want, like, a small
   circle having cross." */
.design-thumb-wrap {
  position: relative;
  display: inline-block;
  margin-block: var(--space-xs);
  /* Backstop, not the primary guarantee: .design-thumb's own fixed 48px box
     below is what normally keeps this wrapper from collapsing. This floor
     covers the case where the <img> element is missing entirely rather
     than merely failing to load -- a collapsed wrapper would make the
     absolutely-positioned Remove control's on-screen position
     unpredictable, which is the one concrete contributor to the round-9
     positioning report (UAT test 16) that CSS alone can remove. */
  min-width: 48px;
  min-height: 48px;
}

/* The ONLY place the thumbnail is sized -- DesignThumbnail
   (internal/customer/preview.go) streams the staged file at its native
   resolution and performs no resampling of its own, deliberately (see
   that handler's doc comment for the accepted tradeoff).

   This is the FIRST build in which any cap on this element actually takes
   effect -- the rule was silently discarded by the browser (a premature
   comment terminator, see this file's header) from 06.1-33 until the
   comment was repaired above. The 96px cap the customer originally reacted
   to was therefore never what they were looking at: with no cap applied,
   the <img> rendered as wide as Pico's own `img { max-width: 100%; height:
   auto }` allowed, i.e. the full controls column.

   48px is a fixed square "glimpse" box, not an auto-sized cap:
   - it is a 4px-scale multiple, consistent with this file's spacing scale
     (matches --space-2xl)
   - it is the conventional small-attachment thumbnail size -- big enough
     to recognise your own artwork, small enough to read as confirmation
     rather than a preview
   - the large compositor preview in the left column remains the real
     preview; this is only a glimpse (the customer's own words: "Just like
     a glimpse")
   - a fixed box gives every slot an identical, predictable 48x48 target
     for the corner Remove control to anchor to, which an auto-sized box
     could not do (a tall narrow upload produced a narrow box and left the
     badge detached from the artwork's visible edge); it also means the
     element still occupies 48x48 if the image ever fails to load

   object-fit: contain letterboxes artwork of any aspect ratio (portrait or
   landscape) inside the fixed box rather than distorting or overflowing
   it. */
.design-thumb {
  display: block;
  width: 48px;
  height: 48px;
  object-fit: contain;
  border: 1px solid var(--pico-muted-border-color);
}

/* Touch-target exception (mirrors the .swatch precedent above): the
   customer explicitly asked for a small circular cross on the thumbnail,
   so this control is a deliberate, narrow exception to the site-wide
   44px minimum (the `button` rule near the top of this file). 24px is
   exactly the 24x24 CSS-pixel minimum of WCAG 2.5.8 (Target Size,
   Minimum) -- the smallest this control may ever legitimately go -- and
   it is also half the 48px .design-thumb edge, which is what keeps it
   from overwhelming the shrunken glimpse. The exception is scoped to this
   one control -- the site-wide rule is unchanged and no other button
   inherits it. */
.design-thumb-remove {
  position: absolute;
  top: 4px;
  inset-inline-end: 4px;
  width: 24px;
  height: 24px;
  min-width: 24px;
  min-height: 24px;
  border-radius: 50%;
  padding: 0;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Confirmed root cause of the invisible-cross bug (UAT round 9, test 16):
     Pico v2's own button rule locally redefines the --pico-background-color
     custom property (and related ones) on every button element, pointing
     it at --pico-primary-background. A var() lookup for that property
     inside any rule that matches a button therefore resolves to the
     primary fill, not the page background. In this project both
     --pico-primary-background and --pico-primary are #1c1b19, so the
     previous var()-based version computed to a charcoal disc with a
     charcoal border and a charcoal glyph -- contrast 1.00:1, an invisible
     cross. Class specificity does win the background-color property
     against Pico's own [type=button] selector on source order; it cannot
     change what the custom property itself resolves to on that element.
     Only a literal can. Do not reintroduce a var() color reference here.

     Literal colours, not theme tokens, and specifically these two:
     - White disc against charcoal glyph is roughly 17:1 contrast, far
       above WCAG AA for any glyph weight.
     - This control sits on top of customer-supplied artwork of unknown
       colour, so the disc must be self-delineating in BOTH directions:
       the white disc separates it from dark artwork, and the charcoal
       ring separates it from white or near-white artwork. Neither alone
       is sufficient -- the two-tone treatment is what makes it work over
       an arbitrary upload.
     - It stays correct if a dark theme is ever added, because an opaque,
       self-contained badge does not depend on what is behind it. */
  background-color: #ffffff;
  border: 1px solid #1c1b19;
  color: #1c1b19;
}

/* Catalog checkout logo thumbnail, sized larger than the shared 48px
   "glimpse" box above (owner request, live-UAT follow-up, 2026-09-08:
   "can we make this logo little big?"). Scoped to #checkout-logo-control
   specifically -- NOT a change to .design-thumb/.design-thumb-remove
   themselves, which stay at their existing 48px/24px sizing everywhere
   else (/custom-upload's own front/back/logo glimpse thumbnails), per
   the explicit customer decision recorded at .design-thumb's own rule
   above ("Just like a glimpse"). This is a different screen with a
   single logo field and more room around it, not a revision of that
   earlier decision. 96px is an exact doubling -- still a 4px-scale
   multiple, consistent with this file's spacing scale -- and the Remove
   badge doubles proportionally (48px, 8px inset) so it keeps the same
   relative size and corner position against the larger box. */
#checkout-logo-control .design-thumb-wrap {
  min-width: 96px;
  min-height: 96px;
}
#checkout-logo-control .design-thumb {
  width: 96px;
  height: 96px;
}
#checkout-logo-control .design-thumb-remove {
  top: 8px;
  inset-inline-end: 8px;
  width: 48px;
  height: 48px;
  min-width: 48px;
  min-height: 48px;
  font-size: 1.25rem;
}

/* Hides a slot's native file input once a design is staged for it
   (customUploadDesignSection's designInputHiddenClass). Once
   .design-thumb-wrap above confirms an upload, the browser's own "no file
   selected" text next to "Choose File" doesn't just sit there redundantly --
   it visibly contradicts the thumbnail, because the out-of-band
   #design-section re-render on every staging round-trip swaps in a BRAND NEW
   <input>, and no markup or JS can ever set a file input's value. display:
   none (not visibility/opacity) is required here specifically because a
   <label for=...> still opens the file picker for a display:none target in
   every major engine, which is exactly what .design-thumb-change below relies
   on -- an approach that would break if this used `hidden` (the HTML
   attribute skips the label association entirely in some engines) instead of
   this class. */
.design-input-hidden {
  display: none;
}

/* Reopens the (now-hidden) file input for a slot that already has a staged
   upload, without requiring the customer to hit Remove first. A <label>,
   not a <button>, because its only job is the native for-id file-picker
   trigger every browser already gives a label for free -- no click handler
   needed. Underlined link styling (not a button) keeps it visually
   subordinate to the thumbnail and its Remove control, which stay the
   primary controls in this wrapper. */
.design-thumb-change {
  display: inline-block;
  margin-inline-start: var(--space-sm);
  font-size: var(--text-label-size);
  text-decoration: underline;
  cursor: pointer;
}

.configurator-controls-col label {
  display: block;
  margin-top: var(--space-md);
}

/* Top-level field-group rhythm on /custom-upload (2026-08-25 live UAT:
   "everything is stacked up one after the other with no proper space...
   really closely, tightly packed"). Every direct child of the form --
   each <section> (Garment, Design, Text, Logo Placement), the size-chart
   disclosure, the loose helper <p> between Garment and Design, and the
   #design-warnings block -- gets generous top spacing so each group reads
   as its own visually distinct cluster, on top of (not instead of) the
   existing tighter label-to-its-own-control spacing above. first-child
   avoids a leading gap before the very first field group. */
#custom-upload-form > * {
  margin-top: var(--space-xl);
}

#custom-upload-form > *:first-child {
  margin-top: 0;
}

/* ============================================================
 * Checkout theme (Phase 7, 07-UI-SPEC.md) — a scoped, self-contained
 * dark palette applied ONLY inside .checkout-theme, wrapping the main
 * content of every checkout screen. Shared chrome (header/footer, styled
 * above) is deliberately NOT re-themed here -- this is an accepted visual
 * seam per the phase's Explicit Sequencing Override, not an oversight.
 *
 * Fonts: Anton (display headings only, one static weight) and Space
 * Grotesk (body/UI, weights 400 and 700), both OFL-licensed and
 * self-hosted from static/fonts (see static/embed.go). font-display:
 * swap plus a system-font fallback in every stack below means a slow or
 * failed font fetch degrades checkout rather than blocking it.
 * ============================================================ */

@font-face {
  font-family: 'Anton';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/Anton-Regular.woff2") format("woff2");
}

/* Space Grotesk 400/700 each ship as TWO @font-face rules, not one --
   basic Latin (the vendored *-Regular.woff2/*-Bold.woff2 files) plus a
   Latin Extended-A/currency-symbols subset (the *-LatinExt.woff2 files).
   This split is load-bearing, not decorative: formatRupees()
   (templates/customer/shared_fragments.templ) renders the checkout
   Order Summary's totals with a literal RUPEE SIGN (U+20B9), which the
   basic-Latin subset does NOT contain -- it lives in the Latin
   Extended-B/currency-symbols block. Without the second rule and its
   unicode-range, U+20B9 would silently fall back to the browser's
   system font, printing a mismatched currency glyph directly beside
   this file's own tabular-nums digits on the single screen where every
   customer confirms how much they're about to pay. Both unicode-range
   values are copied verbatim from Google Fonts' own served CSS, not
   guessed. */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/SpaceGrotesk-Regular.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/SpaceGrotesk-Regular-LatinExt.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/static/fonts/SpaceGrotesk-Bold.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/static/fonts/SpaceGrotesk-Bold-LatinExt.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Phase 9 (D-01): the seven brand tokens, the Pico color-variable
   overrides, and this wrapper's own background-color/color declarations
   all promoted to site scope above (plain :root and
   :root:not([data-theme=dark])) -- the page itself is now dark
   everywhere, so this wrapper no longer needs its own theme at all. Only
   its page-level padding remains genuinely checkout-specific layout. */
.checkout-theme {
  padding: var(--space-2xl) var(--space-lg);
}

/* Breathing room (UAT finding): with zero horizontal padding and no
   max-width, headings/buttons/step-track rendered flush against the
   viewport edge on wide screens -- the opposite of BRAND-INTAKE's
   "generous whitespace" instruction. Cap the reading column and center
   it; the vertical padding above still applies at any width. */
.checkout-theme > * {
  max-width: 640px;
  margin-left: auto;
  margin-right: auto;
}

/* Headings: Anton, uppercase, tight tracking. Every font stack below ends
   in a system fallback so a failed font fetch degrades legibly rather
   than breaking layout. */
.checkout-theme h1 {
  font-family: 'Anton', Impact, sans-serif;
  font-size: 32px;
  line-height: 1.2;
  text-transform: uppercase;
}

.checkout-theme h2,
.checkout-theme h3 {
  font-family: 'Anton', Impact, sans-serif;
  font-size: 20px;
  line-height: 1.2;
  text-transform: uppercase;
}

/* Eyebrow / micro-label role (Phase 9, D-01) -- step indicators and the
   step-progress readout label everywhere, plus any other site-wide
   micro-label (.sp-eyebrow). Never used for form field labels -- see the
   site-wide label rule above, which deliberately narrows BRAND-INTAKE's
   literal "labels are 11px" to eyebrow/micro-labels only. */
.checkout-eyebrow,
.sp-eyebrow {
  font-family: 'Space Grotesk', system-ui, sans-serif;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.4;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* Site-wide table rules (Plan 09-04, D-01): Pico's own table borders derive
   from variables this file's light-mode repoint block does not touch, so
   they would otherwise render near-invisible against the void background.
   This reaches every <table> element site-wide -- the account order pages
   and every admin table this plan targets, but also the customer size-
   chart table (unaffected, welcome). Phase 7's checkout flow itself still
   renders no <table> element of its own, but as of 2026-08-31 the catalog
   detail page (/catalog/{id}, inside .checkout-theme) embeds the shared
   SizeChart component, so this rule now also covers that table -- using
   the same site-wide --sp-cream/--sp-border tokens the size-chart table on
   /custom-upload already relies on, not a separate treatment. */
table {
  width: 100%;
  border-collapse: collapse;
}

th,
td {
  border-bottom: 1px solid var(--sp-border);
  padding: var(--space-sm);
  text-align: left;
  color: var(--sp-cream);
}

/* Column headers read as the eyebrow micro-label role (values mirrored from
   .sp-eyebrow above, not shared via selector list, so a future edit to one
   doesn't silently drift the other without a diff) so headers stand apart
   from the 16px body-role data cells beneath them. */
thead th {
  font-family: 'Space Grotesk', system-ui, sans-serif;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.4;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* Order-status label (Plan 09-04): a bordered, cream-on-card treatment --
   never colored text. Ember text on the card surface computes to 4.17:1
   and fails WCAG AA, the same reason the warning/error treatments above
   use a border rather than a text color. */
.status-pill {
  display: inline-flex;
  align-items: center;
  padding: var(--space-xs) var(--space-sm);
  border: 1px solid var(--sp-border);
  background-color: var(--sp-card);
  color: var(--sp-cream);
  font-size: var(--text-label-size);
}

/* Form controls (Phase 9, D-01): card-tinted background, the corrected
   40% border, cream text -- promoted site-wide from Phase 7's
   checkout-scoped rule. */
input,
select,
textarea {
  background-color: var(--sp-card);
  border: 1px solid var(--sp-border);
  color: var(--sp-cream);
}

/* Focus ring (Phase 9, D-01): amber, fixed regardless of any other control
   state -- per BRAND-INTAKE's "interactive colours never change" rule.
   Promoted site-wide from Phase 7's checkout-scoped rule; applies to
   every interactive element on every page. */
:focus-visible {
  outline: 2px solid var(--sp-amber);
  outline-offset: 2px;
}

/* Primary action buttons (Phase 9, D-01: the hero CTA, plus every
   checkout/roster primary action -- "Order for Myself", "Continue", "Add
   Person", "Pay Now"): ember fill with VOID text, not cream -- cream-on-
   ember computes to 3.80:1 and fails WCAG AA for normal-size text; void-
   on-ember is 4.55:1 and passes, with bold (700) weight as the safety
   margin. See 07-UI-SPEC.md Accessibility and Contrast Finding 2. */
.checkout-btn-primary,
.sp-btn-primary,
.hero-cta {
  background-color: var(--sp-ember);
  border-color: var(--sp-ember);
  color: var(--sp-void);
  font-family: 'Space Grotesk', system-ui, sans-serif;
  font-weight: 700;
  /* Geometry (Phase 7 Plan 03): an <a role="button"> gets Pico's button
     display/padding by default, but this project never trusts a
     third-party default to actually clear the 44px touch-target floor --
     explicit min-height + centering is the same discipline every other
     44px-target control in this codebase already follows. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 var(--space-md);
  text-decoration: none;
}

/* 09-08 checkpoint follow-up: the display:inline-flex above never
   actually applied on any <a role="button"> use of these classes
   (.hero-cta and checkout's "Order for Myself"/"Order for a Group"
   links) -- pico.min.css's own `a[role=button]{display:inline-block}`
   is MORE specific ((0,1,1): one type selector + one attribute
   selector) than a bare class selector ((0,1,0)), so Pico silently won
   and the button fell back to inline-block, where the align-items/
   justify-content centering above is simply inert (they only apply to
   flex/grid containers) and the label sits at its default inline
   position instead of centered -- exactly the "text isn't in the
   middle" the user reported. <button type="submit"> uses of these same
   classes (checkout's "Continue"/"Place Order") were never affected --
   the browser's UA stylesheet sets button's default display with zero
   specificity, so the class rule above already won there. This
   supplementary rule only needs to out-specify Pico's a[role=button]
   selector, not redeclare every property. */
a[role="button"].checkout-btn-primary,
a[role="button"].sp-btn-primary,
a[role="button"].hero-cta {
  display: inline-flex;
}

/* Escape-hatch link-button (260807, UAT finding): the catalog detail
   page's "Set up a roster instead" control used .checkout-btn-secondary --
   the SAME class "Add Another Size" uses -- so despite doing completely
   different things (one adds a row to THIS order, the other leaves the
   single-item flow entirely for the team/roster flow) the two rendered as
   visually identical filled buttons. User feedback: "why do we need 2
   doing the same thing?" -- they don't; they only LOOKED like they did.
   .checkout-btn-link demotes an action to plain underlined text (no fill,
   no border) so it reads as "a different path," matching this codebase's
   own established convention that a plain-text CTA (vs. a filled one)
   gets an underline, not a background (see the hover-treatment comment
   above this rule for the filled-button side of that same convention).
   Still a real <button type="submit">, not an <a> -- it must submit the
   surrounding form with its own name/value pair -- so min-height/padding
   keep it clearing the 44px touch-target floor despite having no visible
   button chrome. */
.checkout-btn-link {
  background: transparent;
  border: none;
  color: var(--sp-accent);
  font-family: 'Space Grotesk', system-ui, sans-serif;
  font-weight: 400;
  text-decoration: underline;
  min-height: 44px;
  padding: 0;
  cursor: pointer;
}

.checkout-btn-link:hover,
.checkout-btn-link:focus {
  color: var(--sp-ember);
}

/* Separates the roster escape hatch from the Continue button above it --
   without this, the question-plus-link reads as if it continues directly
   off Continue's bottom edge rather than as its own, distinct prompt. */
.catalog-roster-prompt {
  margin-top: var(--space-lg);
}

/* D-01 mode-select body (Plan 07-03): stacks the two mode buttons with
   the UI-SPEC-specified --space-md gap. Without this rule, CheckoutModeSelect's
   two <a class="checkout-btn-primary"> links have no layout of their own
   and sit with no deliberate spacing between them. */
.checkout-theme .checkout-mode-select {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  align-items: flex-start;
}

/* Roster "how many shirts?" quantity control (a single number field,
   checkout.go's row_action=set_count): a flex row so the label and input
   sit inline instead of the input defaulting to Pico's full-width block
   layout (the same reason .checkout-mode-select above exists). */
.checkout-theme .checkout-roster-count {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}

.checkout-theme .checkout-roster-count input[type="number"] {
  width: 4.5rem;
  height: 2.5rem;
  margin-bottom: 0;
  padding: 0 var(--space-xs);
  text-align: center;
  /* Tells the browser this field sits on a dark background, so it renders
     native UA chrome -- the number spinner's up/down arrows, in this
     case -- in a dark-appropriate palette instead of assuming a light
     page (UAT finding: the arrows were unstyled and rendered near-
     invisible dark-on-dark). This is the standards-track fix; the
     alternative (hiding the native spinner and hand-building -/+ buttons)
     was tried and reverted per user preference for the simpler, single-
     control version. */
  color-scheme: dark;
}

/* Warning treatment (Phase 9, D-01; originally D-06 duplicate jersey
   number, ROSTER-04's persistent reminder): signal through a left rule,
   never through text color -- ember text on the card surface computes to
   4.17:1 and fails WCAG AA for normal-size text. Cream text throughout.
   Promoted site-wide; .sp-warning is the general-purpose name, .checkout-
   warning stays as an alias so no checkout templ file needs editing. */
.checkout-warning,
.sp-warning {
  border-left: 3px solid var(--sp-ember);
  color: var(--sp-cream);
  padding: var(--space-xs) var(--space-sm);
}

/* Error treatment (Phase 9, D-01; originally D-02/D-03 jersey-number
   format hard-block): a full boundary, not colored text, for the same
   contrast reason as the warning treatment above. Promoted site-wide;
   .error is the pre-existing bare class already used by
   templates/customer/custom_upload.templ's validation banner, which had
   no rule of its own until now. */
.checkout-error,
.sp-error,
.error {
  border: 2px solid var(--sp-ember);
  color: var(--sp-cream);
  padding: var(--space-xs) var(--space-sm);
}

/* Locked material display at checkout (2026-08-07, Phase 10 follow-up):
   a custom-upload order already chose material on /custom-upload, so the
   checkout entry screen shows the choice as plain text instead of a second
   editable <select> -- muted (--sp-cream-secondary, 60%) rather than full
   --sp-cream so it visually reads as "already decided" next to the
   still-editable size/quantity controls beside it, without a border (this
   isn't an error/warning treatment, just a value display). */
.checkout-material-locked {
  color: var(--sp-cream-secondary);
  margin: 0 0 var(--space-sm) 0;
}

/* Success confirmation (2026-08-06, admin/designs.templ's #upload-feedback
   slot): this palette has no dedicated positive color, so amber -- the
   site's one warm accent, --sp-accent -- reused as a full border reads as
   "done" without introducing a new color literal, and stays visually
   distinct from .error's ember border and .warning's left-border-only
   accent treatment above. */
.success {
  border: 2px solid var(--sp-amber);
  color: var(--sp-cream);
  padding: var(--space-xs) var(--space-sm);
}

/* Clear-selection control for a bulk-upload file input (2026-08-06
   feedback): a plain file input has no built-in way to deselect a chosen
   file, so templates/admin/designs.templ's bulkUploadPhotoField pairs
   each input with a small "x" button (JS-driven show/hide, see
   toggleClearButton/clearFileInput in that file). 24x24 matches
   .design-thumb-remove's own WCAG 2.5.8 Target Size Minimum reasoning
   elsewhere in this file, reused here as the smallest this control may
   legitimately go. Unlike .design-thumb-remove, this sits on the page's
   own dark background, never on top of arbitrary uploaded artwork, so it
   uses the page's normal ember/cream treatment instead of that control's
   self-delineating white-disc one -- a different problem with a simpler
   answer. */
.file-input-group {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}

.clear-file-btn {
  width: 24px;
  height: 24px;
  min-width: 24px;
  min-height: 24px;
  padding: 0;
  line-height: 1;
  border: 1px solid var(--sp-ember);
  background: none;
  color: var(--sp-cream);
}

/* Tabular figures (Phase 9, D-01) for every live/frozen numeric value --
   order totals, line prices, quantities, and roster jersey numbers -- so
   they don't jitter on re-render. Promoted site-wide. */
.checkout-numeric,
.sp-numeric {
  font-variant-numeric: tabular-nums;
}

/* Step-progress track: --sp-border background, with the cold-
   purple -> ember -> amber ramp for not-yet-reached / current / completed
   segments. Width and color transitions collapse to instant under
   prefers-reduced-motion -- deliberately NOT frozen at a midpoint, since
   misrepresenting real progress is the opposite of what that preference
   is for. */
.checkout-theme .checkout-step-track {
  background-color: var(--sp-border);
  /* Geometry (Plan 07-03): 07-02 declared only the fill colors below --
     without an explicit height/layout, this track (and each of its empty
     block-level segment divs) resolves to zero height and paints
     nothing. flex + a fixed thin height is the standard accessible shape
     for this exact widget (a row of equal-width segments). */
  display: flex;
  align-items: stretch;
  gap: var(--space-xs);
  height: 4px;
  border-radius: 2px;
  overflow: hidden;
}

.checkout-theme .checkout-step-segment {
  transition: width 250ms ease, background-color 250ms ease;
  flex: 1 1 0;
}

.checkout-theme .checkout-step-segment-pending {
  background-color: var(--sp-cold-purple);
}

.checkout-theme .checkout-step-segment-current {
  background-color: var(--sp-ember);
}

.checkout-theme .checkout-step-segment-complete {
  background-color: var(--sp-amber);
}

@media (prefers-reduced-motion: reduce) {
  .checkout-theme .checkout-step-segment {
    transition: none;
  }
}

/* Heat/reveal chrome (Phase 9 Plan 2, D-02/D-03) -- originally home-page
   only, extended to every customer-facing page post-ship (Layout's heat
   parameter controls whether a given page's markup includes
   .heat-coil/.heat-readout/.heat-progress-rule and loads static/heat.js;
   admin pages opt out). These three elements are pure CSS functions of
   the --heat-t/--heat-f custom properties static/heat.js writes on
   <body>; nothing else in this file writes them. The backdrop
   interpolation itself lives directly on the body rule above (see its
   comment for why the separate .heat-backdrop div this used to be was
   never actually visible).

   UAT gap closure history (Plan 09-07 through the 09-08 checkpoint,
   .planning/debug/resolved/heat-mechanic-invisible.md): the original
   single-declaration backdrop rule interpolated void (#100D12) toward
   --sp-heat-mid-b (#2D130E) directly -- two near-identical near-blacks,
   measured at only 1.11:1 contrast end to end, which is why scrolling
   read as "no visible color change" in UAT. Plan 09-07 widened this to a
   two-stop void->(heat-mid-b blended with an accent color) ramp
   targeting a >=1.82:1 contrast floor; that fix was mathematically
   correct but attached to an element (.heat-backdrop, a position:fixed
   div with z-index:-1) that was never visible in any browser -- the
   real root cause, only found once a real human tested it live. Once
   fixed, the backdrop was rebuilt as the current three-stop void ->
   heat-mid-a -> heat-mid-b path (see the body rule above), matched to
   an external reference document, which deliberately does NOT target
   that same contrast floor -- see body's own comment and
   TestHeatBackdropPerceptualRange for why a self-contrast floor no
   longer applies to the backdrop specifically. */

/* 12px heating coil pinned to the content column's right boundary,
   filling bottom-up as scroll heat rises (BRAND-INTAKE.md). Bottom stop
   is amber (hottest), midpoint ember, top stop cold purple -- clip-path
   reveals more of the gradient from the bottom as --heat-t climbs toward
   1. No glow, no box-shadow, per BRAND-INTAKE's FEEL section.

   UAT gap closure (Plan 09-07): width raised from BRAND-INTAKE's literal
   6px to 12px -- the same class of correction already applied to
   --sp-border (14% -> 40%, see the :root comment above), made here for
   the same reason: the UAT-reported "no colored strip on the right edge"
   finding showed the literal spec value renders too small to notice.

   Position: right:0 (viewport edge). 09-07 briefly moved this inward to
   the centered content column's right boundary on wide viewports (the
   debug session's Lead B diagnosed the raw edge as one reason nobody
   noticed the coil). Reverted back to the flush edge during 09-08
   checkpoint review at the user's explicit request -- the content-column
   inset left a visually distracting empty gap between the coil and the
   window edge that the user disliked more than the original problem it
   fixed. This is a deliberate, informed tradeoff the user made after
   the original diagnosis was explained, not a silent regression. */
.heat-coil {
  position: fixed;
  right: 0;
  top: 0;
  bottom: 0;
  width: 12px;
  /* Above the sticky header's z-index:50 (so the header no longer covers
     the coil's top band -- the debug session's third compounding cause)
     and below the progress rule's 60 and the lightbox's 100, preserving
     the existing ladder (see .heat-progress-rule's comment below). */
  z-index: 55;
  pointer-events: none;
  /* Track (09-08 checkpoint follow-up, matched to an external reference
     document): a static, always-visible cream-at-7% background showing
     the coil's full range even before any of it has filled -- the
     element itself now IS the track; the animated fill moves to
     ::after below so clip-path only ever restricts the fill, never the
     track underneath it. rgba() literal (not color-mix) matches the
     existing --sp-border/--sp-card convention of literal cream-alpha
     values for this exact reason (see the :root comment above). */
  background-color: rgba(255, 241, 214, 0.07);
}

.heat-coil::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Gradient stops matched to the same reference document: cold purple
     at the top (0%), ember at 62% (was a plain 50% midpoint -- no
     accessibility reasoning tied to that number, so free to match),
     amber at the bottom (100%, hottest). Clip-path reveals more of this
     fill from the bottom as --heat-t climbs toward 1; the track above
     is a separate, unclipped layer so it stays visible at the top of
     the coil's range even when --heat-t is still low. */
  background-image: linear-gradient(to top, var(--sp-amber), var(--sp-ember) 62%, var(--sp-cold-purple));
  clip-path: inset(calc((1 - var(--heat-t, 0)) * 100%) 0 0 0);
}

/* Live readout chip riding up the coil (BRAND-INTAKE's "element" reading).
   The word "element" is real markup in the template, aria-hidden along
   with the rest of this atmosphere group; the numeric chip is generated
   content off --heat-f via a CSS counter, so JS never touches any element
   other than <body>.

   UAT gap closure (Plan 09-07): now a real bordered, void-filled chip
   (font-size 11px -> 13px) instead of bare floating text, so it reads at
   a glance instead of disappearing against the page. The opaque void
   fill is deliberate: as --heat-t rises the backdrop warms too (Task 1),
   so an unfilled chip would lose separation from its surroundings right
   when the mechanic is most active -- staying void keeps the chip
   legible across the whole ramp.

   `right` reverted to the flush-edge position alongside .heat-coil
   during 09-08 checkpoint review (see .heat-coil's comment) -- back to
   var(--space-md) from the window edge, same as .heat-coil's own revert,
   so the chip still sits just inboard of the coil at its new (old)
   position. */
.heat-readout {
  position: fixed;
  right: var(--space-md);
  /* Above the sticky header (50), below the progress rule (60) and the
     lightbox (100) -- same ladder position as .heat-coil above. */
  z-index: 56;
  pointer-events: none;
  bottom: calc(var(--heat-t, 0) * 78% + 4%);
  display: flex;
  align-items: baseline;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-sm);
  background-color: var(--sp-void);
  border: 1px solid var(--sp-border);
  color: var(--sp-cream);
  color: color-mix(in srgb, var(--sp-cream) calc((1 - var(--heat-t, 0)) * 100%), var(--sp-amber) calc(var(--heat-t, 0) * 100%));
  font-family: 'Space Grotesk', system-ui, sans-serif;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.4;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-variant-numeric: tabular-nums;
}

/* Numeral leads the chip (09-08 checkpoint follow-up: user reference
   showed a large bold number ahead of a small "°F ELEMENT" label, not
   one flat run of uniform-size text). ::before so it renders ahead of
   the "element" text node in DOM order. Its own font-size/weight/case
   override the parent .heat-readout rule above -- ::before does not
   have to inherit the label's small uppercase/letter-spaced treatment,
   only the color-mix warming, which it does inherit since color isn't
   redeclared here. */
.heat-readout::before {
  counter-reset: heat var(--heat-f, 75);
  content: counter(heat) "\00B0" "F";
  font-family: 'Anton', Impact, sans-serif;
  font-size: 20px;
  text-transform: none;
  letter-spacing: normal;
}

/* Hide the traveling chip on pages using the wide two-column configurator
   grid (.configurator-body/-preview-col/-controls-col -- catalog detail and
   /custom-upload; see that grid's own comment further below). Live-UAT
   catch (260903): .heat-readout rides the FULL page scroll range flush
   against the right window edge (its own `right` rule above), which is
   fine riding over a marketing page's prose or empty background -- the
   documented, accepted "rides over the content" behaviour (see this file's
   pointer-events test comment) -- but .configurator-controls-col's own
   content (material/size/price selectors, and especially the highlighted
   .checkout-price-block card, 260903) sits in that exact same flush-right
   column, so the chip was landing squarely on top of live order/price
   information instead of empty space. Narrower fix than disabling `heat`
   wholesale on these pages (Layout's heat parameter, templates/layout.templ)
   -- the coil graphic and top progress rule neither ride into this column
   nor risk the same collision, so only the chip itself is hidden here; the
   ambient warming effect elsewhere on these two pages is untouched.
   :has() scoping follows the exact same degrade-safely precedent as
   body:has(.heat-coil) elsewhere in this file (see the reduced-motion
   block's own comment) -- an unsupporting engine simply never matches this
   rule and the chip keeps riding as before. */
body:has(.configurator-controls-col) .heat-readout {
  display: none;
}

/* Thin amber progress rule on the sticky nav (BRAND-INTAKE PAGE ORDER item
   1). z-index 60 sits above the sticky header's 50 and below the
   lightbox's 100. */
.heat-progress-rule {
  position: fixed;
  top: 0;
  left: 0;
  height: 2px;
  z-index: 60;
  pointer-events: none;
  background-color: var(--sp-amber);
  width: calc(var(--heat-t, 0) * 100%);
}

/* Reveal-on-viewport-entry (BRAND-INTAKE MOTION section). The hidden state
   is scoped to html.heat-on (a marker static/heat.js adds to <html> as its
   very first action) rather than applying unconditionally, so a visitor
   whose JavaScript never loads at all -- not just a prefers-reduced-motion
   visitor -- sees this content in its visible end state by default,
   instead of permanently hidden with nothing to ever add .revealed. */
[data-reveal] {
  opacity: 1;
  transform: none;
  transition: opacity 300ms ease, transform 300ms ease;
}

html.heat-on [data-reveal] {
  opacity: 0;
  transform: translateY(8px);
}

html.heat-on [data-reveal].revealed {
  opacity: 1;
  transform: none;
}

/* Three-step proof band (BRAND-INTAKE PAGE ORDER item 3): section rules,
   not cards -- a border-top per step, matching BRAND-INTAKE's own
   language.

   UAT gap closure (Plan 09-07): BRAND-INTAKE.md's SIGNATURE MECHANIC list
   names "section rules and section labels, warming along the same ramp"
   as a fourth heat-prominence lever, never built until now -- this puts
   the heat signal INSIDE the content column where a reader's eyes
   actually are, not only at the page edges. border-top-color rides the
   same --heat-t ramp Task 1's backdrop uses, so reason about non-text
   contrast from the endpoints, not a stationary background: at
   --heat-t:0 the border is --sp-border (cream 40% over void, 3.53:1);
   at --heat-t:1 it's opaque amber over Task 1's widened hot backdrop
   (5.48:1); the ramp's minimum is the cold end, which is the state that
   already shipped and already passed. WCAG 1.4.11's 3:1 non-text floor
   holds everywhere with margin. The shorthand above each new declaration
   is its fallback: a parse failure leaves today's static border color
   standing. */
.proof-band {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xl);
  padding: var(--space-2xl) 0;
}

.proof-step {
  flex: 1 1 220px;
  border-top: 1px solid var(--sp-border);
  border-top-color: color-mix(in srgb, var(--sp-border) calc((1 - var(--heat-t, 0)) * 100%), var(--sp-amber) calc(var(--heat-t, 0) * 100%));
  padding-top: var(--space-md);
}

/* Proof-step eyebrow labels warm cream-to-amber with the same ramp,
   scoped to this descendant selector (not the shared .sp-eyebrow rule
   below, which also serves /checkout and /custom-upload) so D-02's
   home-page-only boundary holds. Text on the dark backdrop, never on an
   amber fill -- cream 17.28:1 to amber 10.55:1, both AAA (C-3). */
.proof-step .sp-eyebrow {
  color: var(--sp-cream);
  color: color-mix(in srgb, var(--sp-cream) calc((1 - var(--heat-t, 0)) * 100%), var(--sp-amber) calc(var(--heat-t, 0) * 100%));
}

/* Honest capabilities panel (BRAND-INTAKE PAGE ORDER item 5). Border
   warms with the same ramp as .proof-step above, same reasoning. */
.specs-panel {
  background-color: var(--sp-card);
  border: 1px solid var(--sp-border);
  border-color: color-mix(in srgb, var(--sp-border) calc((1 - var(--heat-t, 0)) * 100%), var(--sp-amber) calc(var(--heat-t, 0) * 100%));
  padding: var(--space-lg);
  margin-block: var(--space-2xl);
}

/* Catalog design-card warming (Phase 9 gap closure, D-02, home page
   only). Root cause at the time this was written: the catalog grid's
   photo tiles left almost no scroll backdrop exposed between them, so
   the backdrop's color change -- correctly computed the whole time --
   had nowhere to be seen over the catalog section specifically. (A
   separate, more fundamental backdrop bug -- the body rule above's
   comment -- was found and fixed afterward; this card-warming stays
   regardless, since it addresses a real, distinct exposure problem.)
   Background blends --sp-card toward body's own three-stop hot value
   (via the same --heat-seg1/--heat-seg2 body derives and this inherits
   -- see body's comment for the full reasoning) so a card's warming
   always matches whatever the page background is doing at the same
   scroll position, just softened by --sp-card's own translucency. */
.design-card {
  padding: var(--space-sm);
  border: 1px solid var(--sp-border);
  border-color: color-mix(in srgb, var(--sp-border) calc((1 - var(--heat-t, 0)) * 100%), var(--sp-amber) calc(var(--heat-t, 0) * 100%));
  background-color: var(--sp-card);
  background-color: color-mix(in srgb, var(--sp-card) calc((1 - var(--heat-t, 0)) * 100%),
    color-mix(in srgb,
      color-mix(in srgb, var(--sp-void) calc((1 - var(--heat-seg1, 0)) * 100%), var(--sp-heat-mid-a) calc(var(--heat-seg1, 0) * 100%))
        calc((1 - var(--heat-seg2, 0)) * 100%),
      var(--sp-heat-mid-b) calc(var(--heat-seg2, 0) * 100%))
    calc(var(--heat-t, 0) * 100%));
}

/* Design-card name: matches h3's exact Anton/uppercase treatment (2026-08-11
   feedback) -- admin's own design list (templates/admin/designs.templ)
   renders a design's name as a plain <h3>, which already gets that styling
   from the h2/h3 rule above; this customer-facing card used <figcaption>
   instead (semantically correct for an image caption, but with no font
   override, so it fell back to plain body text) and read as visibly
   different from the admin's page for the exact same name. Same 220px-plus
   card width class as admin's .gallery-grid, so the same 20px Anton size
   that already works there is a safe, proven fit here too. */
.design-card figcaption {
  padding-top: var(--space-sm);
  font-family: 'Anton', Impact, sans-serif;
  font-size: 20px;
  line-height: 1.2;
  text-transform: uppercase;
}

/* About Us lead paragraph (/about) -- a slightly larger, softened intro
   line under the H1, same role .hero's intro <p> plays on the home page. */
.about-lead {
  font-size: 1.15em;
  color: var(--sp-cream-secondary);
  max-width: 60ch;
  margin-block-end: var(--space-xl);
}

/* About Us contact grid (/about) -- reuses the shop's existing card/border
   tokens (--sp-card, --sp-border) rather than inventing new ones, so the
   page reads as one system with .specs-panel/.design-card. Icon circles use
   --sp-amber fill with --sp-void glyphs: amber (#FFB020) on void (#100D12)
   is a large-margin AAA pairing, no separate contrast check needed.

   Pinned to a deterministic 2-column grid (not auto-fit) so 4 cards always
   land as a clean 2x2 block -- auto-fit let the browser choose a 3+1 split
   at some widths, which read as lopsided/unfinished rather than
   intentional. Collapses to 1 column under the codebase's existing 480px
   mobile breakpoint (see other @media (max-width: 480px) rules below). */
.contact-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-lg);
  margin-block: var(--space-lg) var(--space-2xl);
}

@media (max-width: 480px) {
  .contact-grid {
    grid-template-columns: 1fr;
  }
}

.contact-card {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  background-color: var(--sp-card);
  border: 1px solid var(--sp-border);
  padding: var(--space-lg);
  text-decoration: none;
  color: inherit;
  transition: border-color 0.15s ease;
}

a.contact-card:hover,
a.contact-card:focus-visible {
  border-color: var(--sp-amber);
}

.contact-card-icon {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background-color: var(--sp-amber);
  color: var(--sp-void);
}

.contact-card-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.contact-card-label {
  font-size: 0.78em;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--sp-cream-secondary);
}

.contact-card-value {
  color: var(--sp-cream);
  font-weight: 600;
  overflow-wrap: anywhere;
}

/* Catalog design-card photo box (2026-08-06 feedback): overrides
   .gallery-grid img's shared height/object-fit for this selector only --
   /admin/photos is unaffected (the public gallery portfolio strip this
   comment used to also name was removed by quick task 260810-mdq).
   First attempt kept object-fit: cover and just grew the box height with
   object-position: top -- that only moved the crop from the top edge to
   the bottom edge (any object-fit: cover box crops SOME edge unless its
   aspect ratio happens to match the source photo's, and design photos
   vary in aspect ratio row to row). object-fit: contain removes cropping
   entirely regardless of a photo's aspect ratio, matching the same
   never-crop contract .design-thumb already uses elsewhere in this file
   for the same reason (artwork must letterbox, never overflow or lose
   pixels).

   2026-08-11: briefly tried dropping the fixed height so each image sized
   to its own aspect ratio, to close a gap this box's letterboxing can
   leave above the figcaption -- reverted same day on direct feedback: the
   resulting mismatched card heights across a grid row read as more wrong
   than the gap did. Fixed height + contain stands; the gap this box can
   leave under a short photo is an accepted, known trade-off, not a bug
   to keep re-solving here. */
.design-card img {
  height: 320px;
  object-fit: contain;
}

/* Reduced-motion backstop (BRAND-INTAKE MOTION section: "freeze the heat at
   its mid-point"). Placed after every rule above so these selectors win at
   equal specificity by source order alone -- this file declares zero CSS
   priority-override flags, enforced file-wide by
   TestDesignThumbRemoveUsesLiteralColors. This is defense-in-depth for the
   case where JavaScript never executes at all: the frozen mid-point and
   end-state reveals render correctly with zero script execution, even
   though static/heat.js's own reduced-motion branch also sets these same
   values (and reveals immediately) once it does run.

   UAT gap closure (Plan 09-07): .proof-step and .specs-panel added to
   this selector list now that Task 3 makes them --heat-t readers too --
   every selector that reads --heat-t anywhere in this file is now either
   named here or a descendant of something named here (.proof-step
   .sp-eyebrow inherits the frozen value from .proof-step; custom
   properties inherit down the DOM tree regardless of paint position, so
   it needs no entry of its own). Without this, a reduced-motion visitor
   with no JavaScript at all would see the coil/backdrop/chip frozen at
   the mid-point while the section rules and labels stayed cold -- a
   half-warm mixture this backstop exists to prevent.

   Gap closure (09-08 checkpoint follow-up): .design-card added for the
   same reason -- it became a --heat-t reader when the catalog grid
   gained its own warming background/border, so it needs the same freeze
   or a reduced-motion visitor would see every catalog card mid-warm
   while everything else in this list is correctly frozen. .heat-backdrop
   replaced with body:has(.heat-coil) in this list the same round -- the
   backdrop interpolation now lives directly on body's own background-
   color (see its comment), so body is the selector that needs freezing
   now, not a div that no longer exists.

   Code review catch (09-08 checkpoint, CR-01): a bare `body` selector
   here is wrong, not just imprecise -- body is shared by every page via
   templates/layout.templ, so an unscoped `body { --heat-t: 0.5 }` would
   warm every page's background to the ramp's mid-point for a reduced-
   motion visitor, contradicting body's own rule comment (non-heat pages
   stay flat void). `:has(.heat-coil)` scopes this to whichever pages
   actually render the heat chrome (originally home-only; every
   customer-facing page opts in post-ship via Layout's heat parameter,
   .heat-coil only exists when heat=true) without any JS dependency --
   deliberately NOT html.heat-on, since that class is added BY heat.js
   itself and would never match for the exact zero-JavaScript visitor
   this whole media query exists to serve. :has() degrades safely in an
   unsupporting engine: the selector simply never matches, --heat-t
   stays at its 0 fallback (flat void), never transparent. */
@media (prefers-reduced-motion: reduce) {
  body:has(.heat-coil),
  .heat-coil,
  .heat-readout,
  .heat-progress-rule,
  .proof-step,
  .specs-panel[data-reveal],
  .design-card {
    --heat-t: 0.5;
    --heat-f: 238;
  }

  .heat-coil,
  .heat-readout {
    transition: none;
  }

  html.heat-on [data-reveal] {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* Platen-temperature progress indicator (Phase 9 Plan 06, D-04) --
   /custom-upload's 2-segment climb from 72F to 400F. Structurally
   identical to .checkout-step-track/.checkout-step-segment above --
   same background, flex geometry, and cold-purple -> ember -> amber
   ramp -- deliberately reusing that shape rather than inventing a
   second progress-bar geometry (09-RESEARCH.md Pattern 3). */
.platen-indicator {
  margin-bottom: var(--space-lg);
}

/* Readout typography reuses the eyebrow role's values directly (not
   through the .sp-eyebrow selector list, matching how .platen-indicator
   is its own standalone block) plus tabular figures, since this is a
   live value that changes on every staging/personalization round trip
   and must never jitter (BRAND-INTAKE TYPE section). */
.platen-readout {
  font-family: 'Space Grotesk', system-ui, sans-serif;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.4;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-variant-numeric: tabular-nums;
  color: var(--sp-cream);
  margin-bottom: var(--space-xs);
}

.platen-track {
  background-color: var(--sp-border);
  display: flex;
  align-items: stretch;
  gap: var(--space-xs);
  height: 4px;
  border-radius: 2px;
  overflow: hidden;
}

.platen-segment {
  transition: width 250ms ease, background-color 250ms ease;
  flex: 1 1 0;
}

.platen-segment-pending {
  background-color: var(--sp-cold-purple);
}

.platen-segment-current {
  background-color: var(--sp-ember);
}

.platen-segment-complete {
  background-color: var(--sp-amber);
}

/* This is a DIFFERENT reduced-motion behaviour from the home page's
   heat mechanic above: the heat is atmosphere and freezes at a fake
   mid-point on purpose, but the platen reports real order progress and
   must never misrepresent it -- only the transition collapses to
   instant, the fill itself always reflects the true completed count.
   Placed after the rule it overrides, at equal specificity, so it wins
   on source order alone -- this file declares zero CSS priority-
   override flags (enforced file-wide by
   TestDesignThumbRemoveUsesLiteralColors). */
@media (prefers-reduced-motion: reduce) {
  .platen-segment {
    transition: none;
  }
}

/* Shared page-progress step indicator (Phase 13, StepIndicator,
   templates/customer/step_indicator.templ) -- an unscoped clone of
   .checkout-theme .checkout-step-track/-segment above, painting outside
   any .checkout-theme wrapper for both Phase-13 custom-upload pages.
   Same background, flex geometry, and cold-purple -> ember -> amber ramp
   as checkout's own indicator, kept as a separate .sp-step-* class family
   rather than de-scoping the existing .checkout-step-* selectors (would
   lower their specificity underneath checkout's own live pages for no
   gain). */
.sp-step-indicator {
  margin-bottom: var(--space-lg);
}

.sp-step-track {
  background-color: var(--sp-border);
  display: flex;
  align-items: stretch;
  gap: var(--space-xs);
  height: 4px;
  border-radius: 2px;
  overflow: hidden;
}

.sp-step-segment {
  transition: width 250ms ease, background-color 250ms ease;
  flex: 1 1 0;
}

.sp-step-segment-pending {
  background-color: var(--sp-cold-purple);
}

.sp-step-segment-current {
  background-color: var(--sp-ember);
}

.sp-step-segment-complete {
  background-color: var(--sp-amber);
}

/* Placed after the rule it overrides, at equal specificity, so it wins on
   source order alone -- this file declares zero CSS priority-override
   flags (enforced file-wide by TestDesignThumbRemoveUsesLiteralColors). */
@media (prefers-reduced-motion: reduce) {
  .sp-step-segment {
    transition: none;
  }
}

/* Order-status stepper (Phase 8, 08-05-PLAN.md Task 3, TRACK-01/03/04):
   a 5-node horizontal track on OrderDetailPage, deliberately NOT scoped
   under .checkout-theme -- the order-detail page carries no such wrapper
   element, and .checkout-step-track/.checkout-step-segment are flex block
   segments, not circular nodes, so they are not reused directly here.
   Only the token-to-state color mapping (cold-purple/ember/amber for
   pending/current/complete) carries over from that existing precedent.

   Every fill/outline color below is an opaque, fixed --sp-* token -- none
   read --heat-t or use color-mix(). This page renders with heat=true, so
   the ambient body background interpolates as the customer scrolls; the
   stepper's cold-purple/ember/amber vocabulary means "not started/in
   progress/done" and must stay legible and unambiguous at every scroll
   position, never confused with that separate scroll narrative. */
.status-stepper {
  display: flex;
  align-items: flex-start;
  padding: var(--space-lg) 0;
}

.status-stepper-step {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
  min-width: 0;
}

/* Node geometry: 24px circle -- smaller than the 36px garment-color swatch
   on purpose, since five of these must fit across one phone-width row. */
.status-stepper-node {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  flex-shrink: 0;
  font-weight: 700;
}

.status-stepper-node-complete {
  background-color: var(--sp-amber);
  color: var(--sp-void);
}

.status-stepper-node-current {
  background-color: var(--sp-ember);
}

.status-stepper-node-future {
  background-color: transparent;
  border: 1.5px solid var(--sp-cold-purple);
}

/* Connecting line: takes the state of the node it leads INTO (Task 1's
   stepperLineClass), matching the existing checkout step-track's own
   left-to-right fill logic. Vertically centered against the 24px node via
   a fixed top margin, since the line sits beside the node row, not below
   it -- half the node's height, minus half the line's own thickness. */
.status-stepper-line {
  flex: 1 1 0;
  height: 2px;
  margin-top: 11px;
  min-width: var(--space-sm);
}

.status-stepper-line-complete {
  background-color: var(--sp-amber);
}

.status-stepper-line-current {
  background-color: var(--sp-ember);
}

.status-stepper-line-future {
  background-color: var(--sp-cold-purple);
}

/* Always-visible stage label beneath each node -- reuses the eyebrow/
   micro-label role's exact values (.sp-eyebrow), the same "circle with an
   always-visible label beneath" grammar the garment-color swatches (.swatch
   + .swatch-label) already established. */
.status-stepper-label {
  font-family: 'Space Grotesk', system-ui, sans-serif;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--sp-cream);
  text-align: center;
}

/* Tracking reference (TRACK-04): sits directly beneath the Shipped stage's
   label rather than in a disconnected paragraph above the stepper. */
.status-stepper-tracking {
  font-size: 14px;
  color: var(--sp-cream);
  text-align: center;
  margin-top: var(--space-xs);
  white-space: nowrap;
}

/* Cancellation marker (D-06): a bordered, non-filled treatment -- an
   ember-filled marker with cream text would repeat a contrast failure
   Phase 9's own table already found, so this follows the existing
   .sp-error "border, not fill" convention instead of the complete node's
   solid-fill treatment above. */
.status-stepper-cancelled {
  border: 2px solid var(--sp-ember);
  color: var(--sp-cream);
  padding: var(--space-xs) var(--space-sm);
  font-family: 'Space Grotesk', system-ui, sans-serif;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  white-space: nowrap;
}

/* Reflow to vertical at the project's existing 480px breakpoint (reused
   from .configurator-body/.gallery-grid, no new breakpoint token). */
@media (max-width: 480px) {
  .status-stepper {
    flex-direction: column;
    align-items: flex-start;
  }

  .status-stepper-line {
    width: 2px;
    height: 16px;
    margin: 0 0 0 11px;
  }
}

/* Chronological history log (TRACK-03): a short vertical list beneath the
   stepper, plain body-role text -- a factual list, not an interactive
   component, so it carries no card/border treatment of its own. */
.status-history {
  margin-top: var(--space-xl);
}

.status-history p {
  font-size: 16px;
  color: var(--sp-cream);
  margin: 0 0 var(--space-xs) 0;
}

/* Upload Designs disclosure toggle (/admin/designs, 2026-08-11 feedback):
   Pico's default <summary> renders as a small plain-text line (1rem line
   height, no weight/size bump) -- easy to miss as the page's primary
   action. Sized and weighted up, with the same card/border treatment as
   .specs-panel/.contact-card so it reads as a distinct control, not a
   caption. */
.upload-designs-toggle {
  display: inline-block;
  font-size: 1.3em;
  font-weight: 700;
  color: var(--sp-cream);
  padding: var(--space-sm) var(--space-lg);
  background-color: var(--sp-card);
  border: 1px solid var(--sp-border);
  transition: border-color 0.15s ease;
}

.upload-designs-toggle:hover,
.upload-designs-toggle:focus-visible {
  border-color: var(--sp-amber);
}
