﻿:root {
    /* colors */
    --paper: #ffffff;
    --bone: #f4f3f0;
    --bone-2: #eceae5;
    --ink: #1b1917;
    --black: #0d0c0b;
    --red: #d81e28;
    --red-deep: #a5141c;
    --muted: #6e6a64;
    --line: #e5e2dd;
    /* fonts */
    --font-display: "Sora", "Segoe UI", system-ui, Arial, sans-serif;
    --font-body: "Archivo", "Segoe UI", system-ui, Arial, sans-serif;
    --font-mono: "Quantico", ui-monospace, "Cascadia Code", Consolas, monospace;
    /* font sizes */
    --accent-scale: 1.2;
    --accent-track: .75;
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    font-family: var(--font-body);
    color: var(--ink);
    background: var(--paper);
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
}

a { color: inherit; }

h1, h2, h3, h4 { font-family: var(--font-display); text-wrap: balance; margin: 0; letter-spacing: -.02em; }

.container { max-width: 1140px; margin: 0 auto; padding-inline: 1.5rem; }

main.container { padding-block: clamp(2rem, 5vw, 3.5rem); }

.eyebrow {
    font-family: var(--font-mono);
    font-size: calc(.72rem * var(--accent-scale));
    letter-spacing: calc(.18em * var(--accent-track));
    text-transform: uppercase;
    color: var(--red);
    margin: 0;
}

.muted { color: var(--muted); }

/* ---------- Header ---------- */
.hdr {
    position: sticky; top: 0; z-index: 30;
    background: rgba(255, 255, 255, .9);
    backdrop-filter: saturate(180%) blur(8px);
    border-bottom: 1px solid var(--line);
}
.hdr-in { display: flex; align-items: center; justify-content: space-between; height: 68px; }
.brand { display: flex; align-items: center; gap: .6rem; text-decoration: none; }
/* Both rendered badges -- site-header.png in the header, site-footer.png in the footer -- 512x128
   on a transparent film. One rule, because they are the same art at the same size and differ only
   in which of the two the page asks for.
   Sized by HEIGHT with width:auto: the art is a fixed 4:1, so pinning the height is what holds it
   steady, and any width hard-coded here would just be that ratio restated. The PNG carries ~4% of
   its own transparent margin, so 64px of box reads as roughly 60px of badge -- which in the 68px
   header bar is about 4px of clearance top and bottom. */
.brand-badge { height: 64px; width: auto; display: block; }
.nav { display: flex; align-items: center; gap: 1.6rem; }
.nav a { font-size: .92rem; text-decoration: none; color: var(--ink); opacity: .82; }
.nav a:hover { opacity: 1; color: var(--red); }
.nav .cart { font-family: var(--font-mono); font-size: calc(.8rem * var(--accent-scale)); opacity: 1; border: 1px solid var(--line); border-radius: 999px; padding: .3rem .7rem; }
.nav .cart:hover { border-color: var(--ink); color: var(--ink); }
/* Header pickers -- the printer and, beside it, My Colors. Same control, two axes of the same decision
   (what we cut to / what it's cut in), so they share every base rule; only the unset-printer call-out
   below is printer-specific. */
/* Stacked: icon + caption on top, the control itself underneath. Side by side, the caption competed with
   the value for the same line and the two pickers ran together into one strip of text. */
.hdr-pick { display: inline-flex; flex-direction: column; align-items: flex-start; gap: .22rem; font-size: .8rem; }
.hdr-pick .pp-top { display: inline-flex; align-items: center; gap: .35rem; }
.hdr-pick .pp-row { display: inline-flex; align-items: center; gap: .45rem; }
.hdr-pick .pp-ico { font-size: 1rem; opacity: .75; }
.hdr-pick .pp-lbl { font-family: var(--font-mono); font-size: calc(.68rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.06em * var(--accent-track)); color: var(--muted); }
.hdr-pick select, .cs-trigger { font-family: var(--font-mono); font-size: calc(.78rem * var(--accent-scale)); color: var(--ink); background: transparent; border: 1px solid var(--line); border-radius: 999px; padding: .28rem .6rem; max-width: 15rem; cursor: pointer; }
.hdr-pick select:hover, .cs-trigger:hover { border-color: var(--ink); }
/* two pickers plus the account/cart links is a lot of nav; close the gap between the pair so they read as
   one pair of settings and leave the rest of the bar its breathing room. */
.nav .hdr-pick + .hdr-pick { margin-left: -.8rem; }
/* ---------- the colors listbox ----------
   A <select> can't show a scheme, only name it: an <option> holds text, never markup. So this is a
   listbox, styled to sit in the header as the printer's <select> does -- same pill, same type. */
.color-pick { position: relative; }
.cs-trigger { display: inline-flex; align-items: center; gap: .4rem; max-width: 13rem; }
.cs-cur { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cs-caret { font-size: .6rem; opacity: .6; }
.cs-trigger[aria-expanded="true"] { border-color: var(--ink); }

.cs-menu { position: absolute; z-index: 70; top: 100%; right: 0; margin-top: .3rem; width: 17rem; max-height: 22rem; overflow-y: auto; padding: .3rem; border: 1px solid var(--line); border-radius: 10px; background: #fff; box-shadow: 0 14px 40px rgba(20, 20, 24, .18); }
.cs-grp { position: sticky; top: 0; padding: .45rem .55rem .3rem; font-family: var(--font-mono); font-size: calc(.62rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.08em * var(--accent-track)); color: var(--muted); background: #fff; }
.cs-opt { display: flex; align-items: center; gap: .55rem; padding: .34rem .55rem; border-radius: 7px; font-size: .84rem; cursor: pointer; }
.cs-opt.is-active { background: #f3f2ef; }
.cs-opt[aria-selected="true"] { font-weight: 700; }
.cs-dots { display: inline-flex; gap: .18rem; flex: none; }
.cs-dots i { width: .78rem; height: .78rem; border-radius: 50%; border: 1px solid rgba(20, 20, 24, .18); }
/* the mixer row before anything has been mixed -- three holes where a scheme would be */
.cs-dots i.is-empty { border-style: dashed; border-color: #b9b5ae; }
.cs-onm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the chosen scheme's three colors, beside the select -- the only on-screen proof the pick did anything
   before you open a product page. Absent entirely when no scheme is set. */
.pp-sw { display: inline-flex; gap: .22rem; }
.pp-sw i { width: .82rem; height: .82rem; border-radius: 50%; border: 1px solid rgba(20, 20, 24, .18); }

/* The color mixer. It borrows the add-printer dialog's shell (.ap-dlg) and the product page's own
   color controls (.cfg-colors), so there is nothing new to style but the padding the shell normally
   gets from its <form> -- this dialog has no form to hang it on. */
.mc-body { padding: 1.4rem 1.5rem 1.2rem; }
.mc-colors { gap: 1.6rem; margin: 1.1rem 0 1.4rem; }
.mc-colors input[type=color] { width: 64px; height: 40px; }
/* The caption used to be hidden here -- it sat on the select's line and was the first thing to give up
   the width. Stacked it costs no width, so it stays and only the select narrows. */
@media (max-width: 640px) { .hdr-pick select, .cs-trigger { max-width: 9.5rem; } .cs-menu { width: 15rem; } }

/* ---------- "You haven't picked a printer yet" ----------
   The picker decides what every configurable model is cut to, but it sits in the header nav where the eye
   files it as chrome. Unset, on the home page, it stops being chrome: brand-red ring, red label, and a slow
   halo that draws the eye without flashing at it. It reverts to the quiet default the moment a printer is
   chosen (the pick reloads the page), so this can never become permanent wallpaper. */
.printer-pick.needs-printer .pp-lbl { color: var(--red); }
.printer-pick.needs-printer .pp-ico { opacity: 1; }
/* wider than the default 15rem: the bold "Not set - pick your printer" label is the whole point, and
   at the default width the select clips it to "pick your printe". */
.printer-pick.needs-printer #printerPick { border-color: var(--red); color: var(--red); font-weight: 600; max-width: 18rem; box-shadow: 0 0 0 3px rgba(216, 30, 40, .12); }
.printer-pick.needs-printer #printerPick:hover { border-color: var(--red-deep); }
.printer-pick.needs-printer #printerPick { animation: pp-halo 2.6s ease-in-out infinite; }
@keyframes pp-halo {
    0%, 100% { box-shadow: 0 0 0 3px rgba(216, 30, 40, .12); }
    50% { box-shadow: 0 0 0 6px rgba(216, 30, 40, .04); }
}
/* the ping fired when they click "Pick your printer" in the hero */
.printer-pick.is-pinged #printerPick { animation: pp-ping .55s ease-out 2; }
@keyframes pp-ping {
    0% { box-shadow: 0 0 0 0 rgba(216, 30, 40, .45); }
    100% { box-shadow: 0 0 0 10px rgba(216, 30, 40, 0); }
}
/* ---------- "How many colors can it print?" ----------
   Rides in the printer picker's row, so it takes the same pill and the same nudge -- but narrower:
   it is the second control on a row that already carries a 30-model list, and its three answers are
   short. The ring is a separate class from needs-printer because the two never fire together (see
   MulticolorPickClass), and because needs-printer had to stop matching every <select> in the block
   the moment there were two of them. */
.mc-sel { max-width: 11rem; }
@media (max-width: 640px) { .mc-sel { max-width: 8rem; } }
.printer-pick.needs-multicolor .pp-lbl { color: var(--red); }
.printer-pick.needs-multicolor .pp-ico { opacity: 1; }
.printer-pick.needs-multicolor #mcPick { border-color: var(--red); color: var(--red); font-weight: 600; max-width: 14rem; box-shadow: 0 0 0 3px rgba(216, 30, 40, .12); }
.printer-pick.needs-multicolor #mcPick:hover { border-color: var(--red-deep); }
.printer-pick.needs-multicolor #mcPick { animation: pp-halo 2.6s ease-in-out infinite; }
/* the ping fired when they click "Answer that" in the hero */
.printer-pick.is-pinged-mc #mcPick { animation: pp-ping .55s ease-out 2; }

/* ---------- "You haven't picked your colors yet" ----------
   The same call-out as the printer above, on the control next to it, and deliberately the same
   animations rather than a second set: two nudges that behave differently read as two unrelated
   problems. It differs only in what it dresses -- the colors control is a button and its own listbox,
   not a <select> -- and in WHEN, which is ColorPickClass: never while the printer is still unset. */
.color-pick.needs-colors .pp-lbl { color: var(--red); }
.color-pick.needs-colors .pp-ico { opacity: 1; }
.color-pick.needs-colors .cs-trigger { border-color: var(--red); color: var(--red); font-weight: 600; box-shadow: 0 0 0 3px rgba(216, 30, 40, .12); }
.color-pick.needs-colors .cs-trigger:hover { border-color: var(--red-deep); }
.color-pick.needs-colors .cs-trigger { animation: pp-halo 2.6s ease-in-out infinite; }
/* the ping fired when they click "Pick your colors" in the hero */
.color-pick.is-pinged .cs-trigger { animation: pp-ping .55s ease-out 2; }

@media (prefers-reduced-motion: reduce) {
    .printer-pick.needs-printer #printerPick, .printer-pick.is-pinged #printerPick,
    .printer-pick.needs-multicolor #mcPick, .printer-pick.is-pinged-mc #mcPick { animation: none; }
    .color-pick.needs-colors .cs-trigger, .color-pick.is-pinged .cs-trigger { animation: none; }
}

/* ---------- "Add your printer" dialog (header picker -> "My printer isn't listed...") ---------- */
.ap-dlg { width: min(46rem, calc(100vw - 2rem)); padding: 0; border: 1px solid var(--line); border-radius: 12px; box-shadow: 0 24px 60px rgba(20, 20, 24, .22); color: var(--ink); background: #fff; }
.ap-dlg::backdrop { background: rgba(20, 20, 24, .45); }
.ap-dlg form { padding: 1.4rem 1.5rem 1.2rem; }
.ap-dlg h2 { margin: 0; font-size: 1.3rem; }
.ap-dlg .ap-lede { margin: .45rem 0 1.1rem; color: var(--muted); font-size: .92rem; line-height: 1.5; max-width: 52ch; }
.ap-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: .7rem .9rem; margin-bottom: .8rem; }
.ap-grid label { display: flex; flex-direction: column; gap: .25rem; font-size: .82rem; color: var(--muted); }
.ap-grid input, .ap-grid select { font: inherit; font-size: .92rem; color: var(--ink); padding: .42rem .55rem; border: 1px solid var(--line); border-radius: 7px; background: #fff; }
.ap-grid input:focus, .ap-grid select:focus { outline: 2px solid var(--red); outline-offset: 1px; border-color: var(--red); }
.ap-more { margin: .3rem 0 1rem; border-top: 1px solid var(--line); padding-top: .8rem; }
.ap-more summary { cursor: pointer; font-size: .88rem; font-weight: 600; }
.ap-dlg .ap-note { margin: .6rem 0 .6rem; color: var(--muted); font-size: .84rem; line-height: 1.5; max-width: 60ch; }
/* The feet diagram, and the A/B/C/D badges that tie it to the four boxes. The badge hangs in the
   label's left margin (padding-left + negative text-indent) so a label that wraps to a second line
   lines up under its own text, not under the badge. */
/* The feet are four boxes, and auto-fit lands them 3 + 1 in a 46rem dialog. Pin them to a 2x2 so
   the two SIZES sit on one line and the two foot-center INSETS on the next. Modifier, not a change
   to .ap-grid: the printable w/d/h row above wants all three side by side. */
.ap-grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 620px) { .ap-grid-2 { grid-template-columns: minmax(0, 1fr); } }
.ap-fig { margin: .5rem 0 .9rem; }
.ap-fig svg { display: block; width: 100%; max-width: 24rem; height: auto; }
.ap-fig svg text { font-family: var(--font-body); }
.ap-fig figcaption { margin-top: .3rem; color: var(--muted); font-size: .78rem; }
.ap-lbl { display: block; padding-left: 1.5rem; text-indent: -1.5rem; }
.ap-key { display: inline-block; width: 1.15rem; height: 1.15rem; margin-right: .35rem; border-radius: 50%; background: var(--red); color: #fff; font-size: .72rem; font-weight: 700; line-height: 1.15rem; text-align: center; text-indent: 0; }
/* ---------- the bench ("Manage my printers...") ----------
   One row per printer: name + bed, its own multicolor answer, and a remove. The active row is marked
   rather than moved -- PrinterBeds already keeps the set most-recently-used first, so the top row IS
   the active one and a second visual reordering would just be a second thing to keep true. */
.mp-list { list-style: none; margin: 0 0 .9rem; padding: 0; display: flex; flex-direction: column; gap: .4rem; }
.mp-row { display: flex; align-items: center; gap: .5rem; padding: .45rem .55rem; border: 1px solid var(--line); border-radius: 9px; }
.mp-row.is-active { border-color: var(--red); background: rgba(216, 30, 40, .05); }
.mp-name { flex: 1 1 auto; display: flex; flex-direction: column; align-items: flex-start; gap: .1rem; text-align: left; background: none; border: 0; padding: .1rem .2rem; border-radius: 6px; cursor: pointer; color: var(--ink); font: inherit; }
.mp-name:hover { background: rgba(20, 20, 24, .05); }
/* the active row's name is disabled, so it must not look like a dead control -- it looks like text */
.mp-name:disabled { cursor: default; background: none; opacity: 1; }
.mp-bed { font-family: var(--font-mono); font-size: .74rem; color: var(--muted); }
.mp-mc { flex: 0 0 auto; font-family: var(--font-mono); font-size: .78rem; color: var(--ink); background: transparent; border: 1px solid var(--line); border-radius: 999px; padding: .22rem .5rem; cursor: pointer; }
.mp-mc:hover { border-color: var(--ink); }
.mp-rm { flex: 0 0 auto; width: 1.7rem; height: 1.7rem; line-height: 1; font-size: 1.1rem; background: none; border: 1px solid transparent; border-radius: 999px; color: var(--muted); cursor: pointer; }
.mp-rm:hover { border-color: var(--red); color: var(--red); }
.mp-link { background: none; border: 0; padding: 0; font: inherit; color: var(--red); text-decoration: underline; cursor: pointer; }
@media (max-width: 640px) { .mp-row { flex-wrap: wrap; } .mp-name { flex: 1 0 100%; } }

.ap-err { margin: 0 0 .8rem; padding: .55rem .7rem; border-radius: 7px; background: rgba(216, 30, 40, .08); border: 1px solid var(--red); color: var(--red-deep); font-size: .88rem; }
.ap-err[hidden] { display: none; }
.ap-actions { display: flex; justify-content: flex-end; gap: .6rem; }
.ap-actions button { cursor: pointer; }

/* Configurator sections -- "Drawer array size and feet" / "Drawer size" / "Printer". Grouping the
   controls by what they decide, rather than one undifferentiated grid of dropdowns. */
.cfg-sec { border: 1px solid var(--line); border-radius: 10px; padding: .9rem 1.1rem 1rem; margin: 0 0 1rem; }
.cfg-sec > legend { padding: 0 .5rem; margin-left: -.25rem; font-family: var(--font-mono); font-size: calc(.7rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.09em * var(--accent-track)); color: var(--muted); }
.cfg-sec .cfg-note:last-child { margin-bottom: 0; }
.cfg-sec input:disabled { background: #f4f3f1; color: var(--muted); cursor: not-allowed; }

/* "Saved <printer>" -- survives the reload the save triggers, so the save isn't silent. */
.bm3d-toast { position: fixed; left: 50%; bottom: 1.6rem; transform: translateX(-50%); z-index: 60; padding: .7rem 1.1rem; border-radius: 999px; background: var(--ink); color: #fff; font-size: .9rem; box-shadow: 0 8px 26px rgba(20, 20, 24, .28); opacity: 1; transition: opacity .6s ease; }
.bm3d-toast.is-out { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .bm3d-toast { transition: none; } }

/* The hero call-out that points at it. */
.printer-nudge { display: flex; align-items: center; flex-wrap: wrap; gap: .6rem .8rem; margin: 1.2rem 0 0; padding: .8rem 1rem; border: 1px solid var(--red); border-left-width: 3px; border-radius: 8px; background: rgba(216, 30, 40, .04); font-size: .92rem; line-height: 1.5; }
.printer-nudge .pn-ico { font-size: 1.15rem; }
.printer-nudge .pn-txt { flex: 1 1 16rem; }
.printer-nudge .pn-btn { flex: none; font: inherit; font-size: .85rem; font-weight: 600; color: #fff; background: var(--red); border: 0; border-radius: 999px; padding: .45rem .9rem; cursor: pointer; }
.printer-nudge .pn-btn:hover { background: var(--red-deep); }

/* ---------- Buttons ---------- */
.btn {
    font-family: var(--font-display); font-weight: 700; font-size: .98rem;
    text-decoration: none; border-radius: 10px; padding: .8rem 1.4rem;
    display: inline-flex; align-items: center; gap: .5rem;
    background: var(--red); color: #fff; border: 1.5px solid var(--red); cursor: pointer;
    transition: transform .12s ease, background .15s ease, border-color .15s ease;
}
.btn:hover { background: var(--red-deep); border-color: var(--red-deep); transform: translateY(-1px); color: #fff; }

.btn-outline {
    font-family: var(--font-display); font-weight: 700; font-size: .95rem;
    background: transparent; color: var(--ink); border: 1.5px solid var(--ink);
    border-radius: 10px; padding: .65rem 1.15rem; cursor: pointer; text-decoration: none;
    display: inline-flex; align-items: center; gap: .5rem; transition: background .15s ease, color .15s ease, transform .12s ease;
}
.btn-outline:hover { background: var(--ink); color: #fff; transform: translateY(-1px); }

.btn-link { background: none; border: none; color: var(--muted); cursor: pointer; text-decoration: underline; font-size: .9rem; padding: 0; font-family: var(--font-body); }
.btn-link:hover { color: var(--red); }

/* ---------- Hero (home) ---------- */
.hero { padding-block: clamp(1rem, 3vw, 2.5rem) clamp(2.5rem, 6vw, 4rem); }
.hero-grid { display: grid; grid-template-columns: 1.05fr .95fr; gap: clamp(2rem, 5vw, 4rem); align-items: center; }
@media (max-width: 900px) { .hero-grid { grid-template-columns: 1fr; } }
.hero h1 { font-weight: 800; font-size: clamp(2.3rem, 5vw, 3.8rem); line-height: 1.03; letter-spacing: -.025em; margin: 1rem 0 0; }
.hero h1 em { font-style: normal; color: var(--red); }
.hero .lede { color: var(--muted); font-size: 1.12rem; max-width: 46ch; margin: 1.25rem 0 0; }
.cta-row { display: flex; flex-wrap: wrap; gap: .8rem; margin-top: 1.9rem; }

.hero-actions { display: flex; justify-content: center; margin-top: .85rem; }
.hero-regen { font-size: .8rem; padding: .5rem 1rem; }
.hero-regen:disabled { opacity: .55; cursor: progress; }
.hero-stage { position: relative; border: 1px solid var(--line); border-radius: 16px; background: linear-gradient(180deg, #fbfaf8, var(--bone)); overflow: hidden; aspect-ratio: 5 / 4; }
.hero-stage canvas { display: block; width: 100%; height: 100%; }
.hero-stage .hero-viewer { position: absolute; inset: 0; z-index: 0; }
.hero-stage .hero-viewer canvas { cursor: grab; }
.hero-stage .hero-viewer canvas:active { cursor: grabbing; }
/* Still render shown until the live model loads. The render is baked on flat white, so the panel
   is white too (no seam around the letterboxed image); it fades out to reveal the 3D stage. */
.hero-stage .hero-poster { position: absolute; inset: 0; z-index: 1; pointer-events: none; background: #fff; transition: opacity .55s ease; }
.hero-stage .hero-poster.is-loaded { opacity: 0; }
.hero-stage .hero-poster img { width: 100%; height: 100%; object-fit: contain; }
.hero-viewer-hint { position: absolute; left: 0; right: 0; bottom: 14px; text-align: center; font-family: var(--font-mono); font-size: calc(.72rem * var(--accent-scale)); letter-spacing: calc(.1em * var(--accent-track)); text-transform: uppercase; color: var(--muted); }
.hero-stage .stage-tag { position: absolute; z-index: 2; pointer-events: none; left: 14px; top: 12px; font-family: var(--font-mono); font-size: calc(.68rem * var(--accent-scale)); letter-spacing: calc(.12em * var(--accent-track)); text-transform: uppercase; color: var(--muted); }
.hero-stage .price-chip { position: absolute; right: 14px; bottom: 14px; background: var(--black); color: #fff; border-radius: 12px; padding: .6rem .85rem; display: flex; align-items: center; gap: .6rem; box-shadow: 0 10px 30px rgba(13, 12, 11, .22); }
.hero-stage .price-chip .n { font-family: var(--font-display); font-weight: 800; font-size: 1.2rem; }
.hero-stage .price-chip .k { font-family: var(--font-mono); font-size: calc(.6rem * var(--accent-scale)); letter-spacing: calc(.1em * var(--accent-track)); text-transform: uppercase; color: #b9b6b0; line-height: 1.2; }

/* ---------- Spec strip ---------- */
.strip { border-block: 1px solid var(--line); background: var(--bone); }

/* ---------- Section heads ---------- */
.section-head { display: flex; align-items: end; justify-content: space-between; gap: 1rem; margin: clamp(2.5rem, 5vw, 4rem) 0 1.75rem; }
.section-head h2 { font-weight: 800; font-size: clamp(1.6rem, 3vw, 2.15rem); }
.section-head p { margin: .4rem 0 0; color: var(--muted); }

/* ---------- Catalog ---------- */
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.25rem; }
@media (max-width: 860px) { .grid { grid-template-columns: 1fr; max-width: 420px; } }

/* The category page's card list. `.card-grid` carried no rule at all, so its cards stacked as
   full-width blocks with nothing between them -- hence a 16/9 image the width of the page, and one
   card butting straight onto the next.

   Now a grid the visitor sizes: 2, 3 or 4 across, remembered in a cookie and rendered server-side so
   it arrives in the chosen shape rather than snapping into it. The image no longer needs holding back
   to 80% -- a column narrower than the page was what the 80% was solving.

   Keyed on `.cat-list`, a class this page adds for the purpose, NOT on `.catalog` -- the home page
   wraps its shelves in a .catalog as well, so `.catalog .card-viz` reached in and shrank every
   shelf card to 80% too. The home page's promo band also uses a bare `.card-grid` and inherits the
   same missing gap; left alone here, but worth a look on its own. */
.cat-list { display: grid; gap: 1.25rem; align-items: start; }
.cat-list.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.cat-list.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.cat-list.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* The choice is a ceiling, not a promise: four 300px cards do not fit a tablet. Step down rather than
   letting the cards squeeze to unreadable. */
@media (max-width: 1180px) { .cat-list.cols-4 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 900px)  { .cat-list.cols-3, .cat-list.cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 620px)  { .cat-list.cols-2, .cat-list.cols-3, .cat-list.cols-4 { grid-template-columns: minmax(0, 1fr); } }

/* The 2/3/4 control, above the grid and right-aligned so it sits out of the reading path. */
.cols-pick { display: flex; align-items: center; justify-content: flex-end; gap: .35rem; margin: 0 0 1rem; }
.cols-lbl { font-family: var(--font-mono); font-size: calc(.68rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.06em * var(--accent-track)); color: var(--muted); margin-right: .15rem; }
.cols-btn {
    font-family: var(--font-mono); font-size: calc(.78rem * var(--accent-scale)); line-height: 1; cursor: pointer;
    color: var(--muted); background: transparent;
    border: 1px solid var(--line); border-radius: 7px; padding: .34rem .6rem;
}
.cols-btn:hover { color: var(--ink); border-color: var(--ink); }
.cols-btn.is-on { color: var(--ink); border-color: var(--ink); background: var(--bone-2); font-weight: 700; }
.cols-btn:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
/* Below the smallest breakpoint the grid is one column whatever is picked, so the control is a lie. */
@media (max-width: 620px) { .cols-pick { display: none; } }

/* Name first, above the picture: on a page you read top-to-bottom the name is what you are scanning
   for, and under the image it arrived after the thing it names. */
.cat-title { padding: 1.15rem 1.3rem .35rem; font-weight: 800; font-size: 1.3rem; }
/* The one-liner that says what the thing IS, before the picture shows it. */
.cat-tagline { margin: 0; padding: 0 1.3rem 1rem; color: var(--muted); font-size: 1rem; line-height: 1.5; }
.cat-card .card-viz { border-top: 1px solid var(--line); }
/* The product's real marketing copy, not the one-line tagline. Authored HTML, so style the paragraphs
   it actually contains rather than assuming a single block. */
.cat-copy { color: var(--muted); font-size: .95rem; line-height: 1.6; }
.cat-copy p { margin: 0 0 .7rem; }
.cat-copy p:last-child { margin-bottom: 0; }
.cat-copy strong { color: var(--ink); }

.card {
    display: flex; flex-direction: column; color: inherit; text-decoration: none;
    border: 1px solid var(--line); border-radius: 14px; overflow: hidden; background: var(--paper);
    transition: box-shadow .18s ease, transform .18s ease, border-color .18s ease;
}
.card:hover { transform: translateY(-3px); box-shadow: 0 16px 40px rgba(13, 12, 11, .09); border-color: #d9d5cf; }
.card-viz {
    position: relative; aspect-ratio: 16 / 9; border-bottom: 1px solid var(--line); display: grid; place-items: center;
    background:
        radial-gradient(circle at 1px 1px, rgba(27, 25, 23, .10) 1px, transparent 0) 0 0 / 16px 16px,
        linear-gradient(180deg, #fbfaf8, var(--bone-2));
}
.card-viz .glyph { width: 46%; height: 46%; }
.card-viz img { width: 100%; height: 100%; object-fit: contain; display: block; }
.card-body { padding: 1.1rem 1.2rem 1.3rem; display: flex; flex-direction: column; gap: .5rem; flex: 1; }
.card h3 { font-weight: 800; font-size: 1.16rem; }
.card .desc { margin: 0; color: var(--muted); font-size: .92rem; flex: 1; }
.card .meta { display: flex; align-items: center; justify-content: space-between; gap: .5rem; margin-top: .4rem; }
.card .price { font-family: var(--font-display); font-weight: 800; font-size: 1.1rem; font-variant-numeric: tabular-nums; }

/* The two prices on a card: print it yourself (left), have us print and ship it (right).
   .meta is already space-between, so the pair needs no positioning -- a product sold only as files
   emits one child and it stays left exactly where the single price always was.

   Wrapping is allowed and is the small-card fallback: two "FROM $189.99" figures plus their icons
   do not fit 280px of shelf card at the old 1.1rem, so the figures step down here and the row is
   permitted to break rather than shrink the type further or clip a price. Each price is nowrap so a
   break can only ever happen BETWEEN the two, never inside one. */
.card .meta { flex-wrap: wrap; row-gap: .35rem; }
.ff { display: inline-flex; align-items: center; gap: .38rem; white-space: nowrap; }
/* 28px on a card, 32px everywhere with room. The printer is the busier of the two marks and
   stops reading much below this -- which is why the row above is allowed to wrap instead of
   the icons being shrunk to make two prices fit a 280px shelf card. */
.ff-ico { width: 28px; height: 28px; flex: none; display: block; }
.card .meta .price { font-size: .98rem; }

.badge {
    font-family: var(--font-mono); font-size: calc(.64rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.08em * var(--accent-track));
    color: var(--red); border: 1px solid var(--red); border-radius: 4px; padding: .18rem .45rem;
    /* On the catalog card this is a flex item in `.card .meta`, so it centers and vertical-align is
       ignored. On the product page it is an inline span trailing the price, where the default
       baseline alignment sits its TEXT on the baseline and lets its padding + border hang below the
       line -- which reads as the badge drooping. `middle` centers the box on the text instead.
       Note this must stay inline: the price there can wrap to two lines, and making the parent a flex
       row drops the badge onto a line of its own. */
    vertical-align: middle;
}

/* ---------- Product detail ---------- */
.back { margin: 0 0 1.25rem; }
.back a { color: var(--muted); text-decoration: none; font-family: var(--font-mono); font-size: calc(.82rem * var(--accent-scale)); }
.back a:hover { color: var(--red); }

/* No cap of its own: the article takes the container's full 1092px row, the same row the home page
   splits. It was 940px, which predates the hero's gallery rail -- the rail is the home page's side
   column and the lead image and copy need the rest, and 940px could not seat all three. */
.product { max-width: none; }
.product h1 { margin: 0 0 .5rem; font-weight: 800; font-size: clamp(1.9rem, 4vw, 2.6rem); }
.price-lg { font-family: var(--font-display); font-size: 1.3rem; font-weight: 800; margin: 0 0 1rem; }
.product .desc { color: var(--muted); margin: 0 0 1.5rem; font-size: 1.05rem; }

/* ---------- Configurator (product page) ---------- */
.configurator { border: 1px solid var(--line); border-radius: 14px; padding: 1.2rem 1.35rem; margin: 1.25rem 0 1.5rem; background: var(--bone); }
.configurator h3 { font-weight: 800; font-size: 1.05rem; margin: 0 0 1rem; }
.cfg-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: .75rem; }
.cfg-grid label { display: flex; flex-direction: column; gap: .3rem; font-family: var(--font-mono); font-size: calc(.74rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.05em * var(--accent-track)); color: var(--muted); }
.cfg-grid select { padding: .5rem .55rem; border: 1px solid #cfccc6; border-radius: 8px; font-size: 1rem; font-family: var(--font-body); background: var(--paper); line-height: 1.35; }
/* Text inputs sitting in a .cfg-grid (e.g. Foot height, laptop Base depth/width) -- the .txt-sm box
   style is otherwise only defined as .cfg-plate .txt-sm, so it never reaches these. Match the selects.

   Both carry an explicit line-height, and that is what makes the two boxes the same HEIGHT. Matching
   padding and font-size is not enough: left at `normal`, an <input> and a <select> compute different
   content heights from the same font, so a select sat ~4px shorter than the number box beside it. */
.cfg-grid .txt-sm { width: 100%; padding: .5rem .55rem; border: 1px solid #cfccc6; border-radius: 8px; font-size: 1rem; font-family: var(--font-body); background: var(--paper); line-height: 1.35; }
/* The second line of a .cfg-grid label: the unit, and where to take the measurement. Lower case and
   unemphasised on purpose -- the label above it is the field, this is the instruction, and a stack of
   two shouting mono lines reads as two fields. */
.cfg-grid .cfg-hint { text-transform: none; letter-spacing: normal; font-weight: 400; opacity: .8; }
.cfg-plate { display: flex; gap: 1rem; margin: .9rem 0 0; flex-wrap: wrap; }
.cfg-plate label { display: flex; flex-direction: column; gap: .3rem; font-family: var(--font-mono); font-size: calc(.72rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.05em * var(--accent-track)); color: var(--muted); }
/* Same explicit line-height as .cfg-grid select / .cfg-grid .txt-sm, and for the same reason: left
   at `normal`, Chrome ignores the page's inherited line-height inside a <select> but honors it in
   an <input>, so the printer select sat visibly shorter than the bed width/depth boxes beside it. */
.cfg-plate .txt-sm { width: 130px; padding: .5rem .55rem; border: 1px solid #cfccc6; border-radius: 8px; font-size: 1rem; font-family: var(--font-body); line-height: 1.35; }
.cfg-plate .cfg-printer select { min-width: 15rem; max-width: 100%; padding: .5rem .55rem; border: 1px solid #cfccc6; border-radius: 8px; font-size: 1rem; font-family: var(--font-body); background: var(--paper); line-height: 1.35; }
/* Combo boxes: drop the browser's native chrome and draw our own caret, so a <select> matches the
   flat text inputs beside it (same border, radius, background, padding) instead of the OS control. */
.cfg-grid select, .cfg-plate .cfg-printer select, .ap-grid select {
    -webkit-appearance: none; -moz-appearance: none; appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none' stroke='%236e6a64' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1.5 1.5 6 6l4.5-4.5'/%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: right .6rem center; padding-right: 1.9rem;
}
.cfg-custom { display: inline-flex; gap: 1rem; flex-wrap: wrap; }
.cfg-note { font-family: var(--font-mono); font-size: calc(.74rem * var(--accent-scale)); color: var(--muted); line-height: 1.5; }
.cfg-colors { display: flex; gap: 1rem; margin: .9rem 0; flex-wrap: wrap; }
.cfg-colors label { display: flex; flex-direction: column; gap: .3rem; font-family: var(--font-mono); font-size: calc(.72rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.05em * var(--accent-track)); color: var(--muted); align-items: flex-start; }
.cfg-colors input[type=color] { width: 46px; height: 32px; border: 1px solid #cfccc6; border-radius: 6px; padding: 0; background: none; cursor: pointer; }
/* The scheme row that replaced the per-product color pickers. Colors are chosen once in the header, so
   a product page shows what it will be built in and offers the one decision that IS per-product: which of
   the three lands on the biggest surface. */
.cfg-scheme { display: flex; align-items: flex-start; gap: .5rem; margin: .9rem 0; flex-wrap: wrap; }
.cs-sw { display: flex; flex-direction: column; align-items: center; gap: .32rem; }
.cs-sw i { width: 46px; height: 32px; border-radius: 6px; border: 1px solid #cfccc6; }
.cs-lbl { font-family: var(--font-mono); font-size: calc(.66rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.05em * var(--accent-track)); color: var(--muted); }
/* nudged down so the arrow sits against the swatches, not the labels under them */
.cs-swap { margin-top: .35rem; padding: .1rem .3rem; font-size: 1.05rem; line-height: 1; color: var(--muted); background: none; border: 1px solid transparent; border-radius: 5px; cursor: pointer; }
.cs-swap:hover { color: var(--ink); border-color: var(--line); }
.cs-swap:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }
/* a slot pointed at another one has no color of its own on show to trade */
.cs-swap:disabled { opacity: .3; cursor: default; }
.cs-swap:disabled:hover { color: var(--muted); border-color: transparent; }
/* "Its own color" / "Same as primary": the one decision a slot past the first can make about itself.
   Sized off the label above it so the picker reads as part of the swatch rather than a form field, and
   left to size ITSELF widthwise -- a cap in rem clips "Same as secondary" at the larger --accent-scale. */
.cs-link { font-family: var(--font-mono); font-size: calc(.66rem * var(--accent-scale)); color: var(--muted);
    padding: .1rem .25rem; background: none; border: 1px solid var(--line); border-radius: 5px; cursor: pointer; }
.cs-link:hover { color: var(--ink); }
/* borrowed, not its own -- so two swatches showing one color still read as two roles */
.cs-sw.is-linked i { border-style: dashed; }

/* The assembled-size readout. Sized like the price, because for someone measuring a shelf it is the
   other number the decision turns on -- mm big, inches under it for the tape most US buyers own. */
.cfg-dims { display: flex; gap: 1.1rem 2.2rem; flex-wrap: wrap; }
.cfg-dims .dim { display: flex; flex-direction: column; gap: .15rem; }
.dim-lbl { font-family: var(--font-mono); font-size: calc(.7rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.07em * var(--accent-track)); color: var(--muted); }
.dim-mm { font-family: var(--font-display); font-weight: 700; font-size: 1.25rem; line-height: 1.2; }
.dim-in { font-family: var(--font-mono); font-size: calc(.76rem * var(--accent-scale)); color: var(--muted); }

.cfg-price { margin: .6rem 0 1rem; font-family: var(--font-display); font-weight: 700; font-size: 1.1rem; }

/* The ovals kit's live shape. Drawn from the same rungs the generator gets, so it is the kit
   rather than an illustration of one -- see Product.aspx's sgo script block. The band is stroked
   in currentColor so it follows the page's ink rather than pinning a colour here. */
.sgo-shape { margin: .9rem 0 .25rem; text-align: center; color: var(--ink); }
.sgo-shape svg { display: block; width: 100%; max-width: 260px; max-height: 300px; margin: 0 auto; }
.sgo-cap { margin: .5rem 0 0; font-family: var(--font-mono); font-size: calc(.76rem * var(--accent-scale)); color: var(--ink); font-variant-numeric: tabular-nums; }

.preview3d { position: relative; width: 100%; height: 400px; margin-top: 1rem; border: 1px solid var(--line); border-radius: 12px; background: radial-gradient(130% 100% at 50% 0%, #ffffff, #f3f2ef); overflow: hidden; }
.preview3d canvas { display: block; }
/* "Show my printer on the stand" -- the translucent printer ghost in the 3D preview. */
.ghost-toggle { display: flex; align-items: baseline; gap: .5rem; margin: .6rem 0 0; font-size: .86rem; line-height: 1.45; cursor: pointer; }
.ghost-toggle[hidden] { display: none; }
.ghost-toggle input { flex: none; }
.ghost-toggle .muted { font-size: .8rem; }
.preview-hint, .preview-status { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; text-align: center; color: var(--muted); padding: 1rem; font-family: var(--font-mono); font-size: calc(.82rem * var(--accent-scale)); }
.preview-status { background: rgba(255, 255, 255, .82); font-weight: 500; color: var(--ink); }
/* The render's progress: a stepped bar plus the generator's own stage text. The stages arrive from
   gen.py's stage() calls, so the wording here is the generator's, not the page's. */
.ps-inner { display: flex; flex-direction: column; align-items: center; gap: .55rem; width: 100%; max-width: 320px; }
.ps-bar { width: 100%; height: 5px; border-radius: 999px; background: rgba(20, 20, 24, .1); overflow: hidden; }
.ps-bar[hidden] { display: none; }
.ps-bar > i { display: block; height: 100%; width: 0; border-radius: inherit; background: var(--red);
              transition: width .45s cubic-bezier(.22, .61, .36, 1); }
/* The stage line is the generator talking, so it is set apart from the page's own opening message:
   smaller, muted, and in the mono face the rest of the viewer's machine-side text uses. */
.ps-stage { font-family: var(--font-mono); font-size: calc(.72rem * var(--accent-scale)); font-weight: 400; color: var(--muted); text-align: center; }
.ps-stage[hidden] { display: none; }
/* Shown only once a render has been going a while -- see NOTE_AFTER_MS in configurator.js. Quieter
   than the stage line above it: it is reassurance, not information, and it must not read as a
   warning that something has gone wrong. */
.ps-note { max-width: 30ch; font-size: calc(.68rem * var(--accent-scale)); font-weight: 400; line-height: 1.45; color: var(--muted); opacity: .85; text-align: center; }
.ps-note[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .ps-bar > i { transition: none; } }
.build-stats { position: absolute; left: 0; right: 0; bottom: 0; display: flex; flex-wrap: wrap; gap: .35rem 1.4rem; justify-content: center; padding: .5rem 1rem; background: rgba(255, 255, 255, .88); border-top: 1px solid var(--line); font-family: var(--font-mono); font-size: calc(.76rem * var(--accent-scale)); color: var(--ink); backdrop-filter: blur(2px); }
.build-stats span { white-space: nowrap; }
.build-stats .build-price { font-weight: 600; color: var(--red); }
/* The viewer's own corner controls, top-right inside the canvas: a legend for the mouse, and the
   full-screen toggle. Built by configurator.js, not written into the markup -- see buildTools(). */
.preview-tools { position: absolute; top: .5rem; right: .5rem; max-width: calc(100% - 1rem); display: flex; align-items: flex-start; justify-content: flex-end; gap: .4rem; pointer-events: none; }
.preview-tools[hidden] { display: none; }
.preview-help { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .05rem .6rem; padding: .3rem .5rem; border: 1px solid var(--line); border-radius: 7px; background: rgba(255, 255, 255, .82); backdrop-filter: blur(2px); font-family: var(--font-mono); font-size: calc(.68rem * var(--accent-scale)); line-height: 1.5; color: var(--muted); }
.preview-help span { display: inline-flex; align-items: center; gap: .28rem; }
.preview-help b { font-weight: 600; color: var(--ink); }
/* The mouse pictograms. Ink, not muted: they are the part of the line being read, and the filled
   button has to separate from the outline around it at 11px. */
.preview-help .ico-mouse { flex: none; width: .95em; height: 1.36em; color: var(--ink); fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linejoin: round; }
.preview-help .ico-mouse .lit { fill: currentColor; stroke: none; }
/* A mouse legend on a phone is three lies, so the touch one takes its place -- the gestures differ,
   the need to be told them does not. Keyed to the PRIMARY pointer, so a tablet reads as touch until
   something with a cursor is attached to it. */
.preview-help .for-touch { display: none; }
@media (pointer: coarse) {
    .preview-help .for-mouse { display: none; }
    .preview-help .for-touch { display: inline; }
}
.preview-full { pointer-events: auto; flex: none; display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; padding: 0; border: 1px solid var(--line); border-radius: 7px; background: rgba(255, 255, 255, .86); backdrop-filter: blur(2px); color: var(--ink); cursor: pointer; }
.preview-full:hover { background: #fff; border-color: var(--ink); }
.preview-full svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
/* Full screen, both flavours. The real one is the container itself, so its own gradient becomes the
   backdrop rather than the browser's black; .is-faux-full is the fallback for when the browser won't
   grant it, and covers the viewport instead of the screen. */
.preview3d:fullscreen, .preview3d.is-faux-full { margin: 0; border: 0; border-radius: 0; width: 100%; height: 100%; }
.preview3d.is-faux-full { position: fixed; inset: 0; width: 100vw; height: 100vh; z-index: 2000; }
/* "won't fit your bed" warning + the gated Add-to-cart it disables */
.fit-warn { margin: .9rem 0 0; padding: .7rem .9rem; border: 1px solid #e6b7ba; border-left: 3px solid var(--red); border-radius: 8px; background: #fdf3f3; color: #7d1a20; font-size: .85rem; line-height: 1.45; }
.fit-warn[hidden] { display: none; }
/* The same notice at the top of a product page, where it has to carry across a hero rather than sit
   inside a configurator: full-strength red rule and body-sized text, because a .85rem note under a
   3rem headline reads as small print about a delivery date rather than "this is the wrong product". */
.fit-warn-lead { margin: 0 0 1.5rem; border-left-width: 4px; font-size: .95rem; }
.fit-warn-lead strong { color: var(--red); }
.fit-warn a { color: #7d1a20; font-weight: 600; }
/* The plate size the rack's slots are cut to. Stated on every rack build, not just the doubtful ones:
   the customer is the only person who can see whether the number matches the sheet in their hand. */
.plate-note { border-left: 3px solid var(--line); padding-left: .7rem; }
.fit-ok { margin: 0 0 1rem; padding: .7rem .9rem; border: 1px solid #bfe0c4; border-left: 3px solid #2f9e44; border-radius: 8px; background: #f2fbf4; color: #1c5a2b; font-size: .9rem; line-height: 1.45; }
.cfg-printer-line { margin: 0 0 1rem; font-size: .95rem; }
.meas-panel { background: #fbfaf8; }
.meas-panel .cfg-grid { margin-bottom: .9rem; }
.meas-panel input[type="text"], .meas-panel input[type="number"] { width: 100%; }
.btn.is-disabled { opacity: .5; cursor: not-allowed; pointer-events: none; }
.cfg-fulfillment { margin-top: 1.1rem; border: 1px solid var(--line); border-radius: 12px; padding: .6rem 1rem 1rem; }
.cfg-fulfillment legend { font-family: var(--font-mono); font-size: calc(.72rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.08em * var(--accent-track)); color: var(--muted); padding: 0 .4rem; }
.cfg-fulfillment .ff-opt { display: flex; gap: .6rem; align-items: flex-start; padding: .55rem .2rem; cursor: pointer; line-height: 1.35; }
.cfg-fulfillment .ff-opt + .ff-opt { border-top: 1px solid var(--line); }
.cfg-fulfillment .ff-opt input { margin-top: .2rem; }
.cfg-fulfillment .ff-price { font-variant-numeric: tabular-nums; }
.cfg-fulfillment .ff-physical input:disabled ~ span { opacity: .55; }

/* The pictograms on the product page: bigger than a card's, because here they are labeling a choice
   the buyer is about to make rather than tagging a number in a grid. The icon is a flex sibling of
   the option's text, so `.ff-physical input:disabled ~ span` dims the mark along with the words it
   belongs to -- an undimmed van beside a greyed-out option reads as a live button. */
.cfg-fulfillment .ff-opt .ff-ico-wrap { flex: none; display: flex; align-items: center; padding-top: .1rem; }
.cfg-fulfillment .ff-ico { width: 32px; height: 32px; }

/* The hero pair: the download price keeps the big type, the printed line sits under it in body size
   and muted, because it is the option a buyer opts into rather than the headline. */
.phero-copy .price-lg .ff { gap: .5rem; }
.phero-copy .price-lg .ff-ico { width: 32px; height: 32px; }
.price-printed { margin: 0 0 1rem; font-size: .95rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.price-printed .ff-ico { width: 26px; height: 26px; }
.cart-fulfil { display: inline-block; font-family: var(--font-mono); font-size: calc(.68rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.06em * var(--accent-track)); padding: .12rem .45rem; border-radius: 4px; margin-left: .4rem; vertical-align: middle; }
.cart-fulfil.physical { background: var(--red); color: #fff; }
.cart-fulfil.digital { border: 1px solid var(--line); color: var(--muted); }
.cart-total.sub { font-weight: 400; color: var(--muted); border-bottom: 0; padding-bottom: .15rem; }
.cart-total.sub .price { font-weight: 400; }
.ship-fields { margin: 1.2rem 0 .4rem; padding: 1rem 1.1rem 1.2rem; border: 1px solid var(--line); border-radius: 12px; display: flex; flex-direction: column; gap: .4rem; }
.ship-fields .ship-h { margin: 0 0 .4rem; font-size: 1rem; text-transform: none; letter-spacing: 0; color: var(--ink); }
.ship-fields .txt { display: block; width: 100%; box-sizing: border-box; }
.ship-row { display: flex; gap: .8rem; }
.ship-row > span { flex: 1; display: flex; flex-direction: column; gap: .4rem; }
.ship-to { margin: .4rem 0 1rem; padding: .8rem 1rem; border: 1px solid var(--line); border-radius: 10px; background: rgba(0,0,0,.02); }
.ship-to-h { font-family: var(--font-mono); font-size: calc(.72rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.08em * var(--accent-track)); color: var(--muted); margin: 0 0 .3rem; }
.ship-to-addr { margin: 0 0 .4rem; line-height: 1.5; }
.calc-ship { display: flex; align-items: center; gap: .8rem; margin: .9rem 0 .2rem; flex-wrap: wrap; }
.ship-svc { font-family: var(--font-mono); font-size: calc(.66rem * var(--accent-scale)); color: var(--muted); }
button.btn[disabled] { opacity: .5; cursor: not-allowed; }

/* ---------- Cart / checkout / order ---------- */
.cart-table { width: 100%; border-collapse: collapse; margin-bottom: 1.5rem; }
.cart-table th, .cart-table td { text-align: left; padding: .8rem .5rem; border-bottom: 1px solid var(--line); }
.cart-table th { font-family: var(--font-mono); font-size: calc(.72rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.08em * var(--accent-track)); color: var(--muted); font-weight: 500; }
.cart-table th.price, .cart-table td.price { text-align: right; font-variant-numeric: tabular-nums; }
/* The line picture. Fixed width so the item column starts in the same place whether or not a product
   has an image, and top-aligned because the line beside it can run to two rows of text. */
.cart-table th.thumb, .cart-table td.thumb { width: 64px; padding-right: 0; vertical-align: top; }
.cart-table a { color: var(--ink); font-weight: 600; text-decoration: none; }
.cart-table a:hover { color: var(--red); }
.cfg-desc { display: block; font-family: var(--font-mono); font-size: calc(.76rem * var(--accent-scale)); color: var(--muted); margin-top: .25rem; }

.summary { list-style: none; padding: 0; margin: 0 0 1.5rem; max-width: 680px; }
.summary li { display: flex; justify-content: space-between; align-items: center; padding: .7rem .2rem; border-bottom: 1px solid var(--line); gap: .5rem; }

.cart-total { display: flex; justify-content: space-between; align-items: baseline; max-width: 680px; font-family: var(--font-display); font-size: 1.2rem; font-weight: 800; padding: .8rem .2rem 1.1rem; }
.cart-total .price { font-variant-numeric: tabular-nums; }
.cart-actions { max-width: 680px; }

.checkout-form { max-width: 560px; margin-top: 1.5rem; display: flex; flex-direction: column; gap: .5rem; }
.checkout-form label { font-family: var(--font-mono); font-size: calc(.74rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.05em * var(--accent-track)); color: var(--muted); margin-top: .7rem; }
.checkout-form .txt { padding: .7rem .75rem; border: 1px solid #cfccc6; border-radius: 8px; font-size: 1rem; font-family: var(--font-body); }
#card-container { min-height: 90px; padding: .2rem 0; }
.checkout-form .btn { margin-top: 1.1rem; align-self: flex-start; }
.field-error { color: var(--red); font-size: .9rem; margin: .3rem 0 0; }

.notice { max-width: 680px; margin-top: 1.5rem; padding: 1rem 1.2rem; background: var(--bone); border: 1px solid var(--line); border-radius: 10px; color: var(--muted); }

.order-box { max-width: 680px; border: 1px solid var(--line); border-radius: 14px; padding: 1.3rem 1.5rem; margin-top: 1rem; }
/* "Your makes" is the only order-box with a heading -- the rest lead with an order number in a <p>. */
.order-box h2 { font-size: 1.05rem; margin: 0 0 .9rem; }
.downloads li { flex-wrap: wrap; gap: .5rem; align-items: center; }
.dl-name { flex: 1 1 55%; font-weight: 600; }
/* Holds several buttons now (all / STLs / 3MF / guide), so it wraps rather than running off a narrow
   screen -- the line's name and price already claim most of the row. */
.dl-action { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: .4rem; justify-content: flex-end; }
.dl-action .btn, .dl-action .btn-outline { padding: .4rem .85rem; font-size: .9rem; }
.gen-status { display: inline-flex; align-items: center; gap: .45rem; color: var(--muted); font-family: var(--font-mono); font-size: calc(.82rem * var(--accent-scale)); }
/* A failed build isn't a dead end: it reads as a note with a way out, not an error shouting in red. */
.gen-failed { display: inline-flex; align-items: center; flex-wrap: wrap; gap: .5rem; color: var(--muted); font-size: .9rem; }
.order-box .btn-link { margin-left: .5rem; }
.spinner { width: 14px; height: 14px; border: 2px solid #cfccc6; border-top-color: var(--red); border-radius: 50%; display: inline-block; animation: bm-spin .8s linear infinite; }
@keyframes bm-spin { to { transform: rotate(360deg); } }

/* ---------- How it works ---------- */
.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }
@media (max-width: 800px) { .steps { grid-template-columns: 1fr; max-width: 460px; } }
.step .bar { height: 3px; background: linear-gradient(90deg, var(--red), transparent); border-radius: 3px; margin-bottom: 1rem; }
.step .num { font-family: var(--font-mono); font-size: calc(.78rem * var(--accent-scale)); color: var(--red); letter-spacing: calc(.1em * var(--accent-track)); }
.step h3 { font-weight: 800; font-size: 1.2rem; margin: .5rem 0 .4rem; }
.step p { margin: 0; color: var(--muted); font-size: .95rem; }

/* ---------- Footer ---------- */
.foot { background: var(--black); color: #cfccc6; margin-top: clamp(3rem, 6vw, 5rem); }
.foot-in { display: grid; grid-template-columns: 1.6fr 1fr 1fr; gap: 2rem; padding-block: 3rem 2rem; }
@media (max-width: 640px) { .foot-in { grid-template-columns: 1fr 1fr; } }
.foot h4 { font-family: var(--font-mono); font-size: calc(.72rem * var(--accent-scale)); letter-spacing: calc(.12em * var(--accent-track)); text-transform: uppercase; color: #86837d; margin: 0 0 .9rem; font-weight: 500; }
.foot ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.foot a { text-decoration: none; color: #cfccc6; font-size: .9rem; }
.foot a:hover { color: #fff; }
.foot-note { border-top: 1px solid rgba(255, 255, 255, .1); padding-block: 1.1rem 1.5rem; font-family: var(--font-mono); font-size: calc(.72rem * var(--accent-scale)); color: #86837d; display: flex; justify-content: space-between; flex-wrap: wrap; gap: .5rem; }
/* The build stamp is machinery, not brand: dimmer than the line it sits on so the eye passes over it
   until someone is looking for it. `help` cursor advertises the tooltip carrying the exact build time. */
.foot-ver { color: #6b6862; cursor: help; }

:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; border-radius: 4px; }

/* ---- product info / marketing page ---- */
html { scroll-behavior: smooth; }

/* Everything above Configure & Buy, beside the gallery rail. The rail is --side-col -- the very same
   width as the home page's Suggestion box column, and by the same arithmetic, because both rows are
   the container's 1092px (see :root).
   The rail is a sibling of the whole left column rather than a column of .phero: as a hero column its
   height became the hero's height, so a product with six shots pushed the marketing copy down past
   all of them. Here it runs beside the hero, the marketing, the features and the makes, and ends
   wherever it runs out -- at Configure & Buy at the latest, which is outside this row. */
.pbody {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, var(--side-col));
    gap: var(--side-gap);
    /* start, not stretch: the left column runs long, and a stretched rail would hand all the
       leftover height to its last figure rather than sit at the top of the row. */
    align-items: start;
}
.pbody.no-rail { grid-template-columns: minmax(0, 1fr); }

/* The lead image is a fixed track rather than a fraction: it is 25% narrower than the 461px it took
   in the old 940px two-column row, and a fraction of a column that has since changed width would
   have quietly given that back. */
.phero { display: grid; grid-template-columns: minmax(0, 346px) minmax(0, 1fr); gap: 2.5rem; align-items: center; margin: .5rem 0 2.75rem; }
.phero.no-media { grid-template-columns: 1fr; max-width: 640px; }
.phero-media { background: var(--paper); border: 1px solid var(--line); border-radius: 16px; padding: 1rem; box-shadow: 0 18px 40px -24px rgba(13, 12, 11, .35); }
.phero-media img { display: block; width: 100%; height: auto; border-radius: 8px; }
.phero-copy h1 { margin: 0 0 .5rem; }
.phero-copy .tagline { font-family: var(--font-display); font-size: clamp(1.1rem, 2vw, 1.3rem); font-weight: 600; line-height: 1.35; color: var(--ink); margin: 0 0 1rem; }
.phero-copy .price-lg { margin: 0 0 .35rem; }
.phero-copy .desc { margin: 0 0 1.5rem; font-size: 1rem; }
.btn-cta { display: inline-block; text-decoration: none; font-size: 1.02rem; padding: .7rem 1.4rem; }

/* The site's long-form measure: product marketing copy, and the About page's prose. One rule under
   two names rather than a second copy of the same numbers -- they are the same thing, a column of
   text meant to be read rather than scanned. */
.pmarketing, .page-copy { max-width: 660px; margin: 0 auto 2.75rem; font-size: 1.08rem; line-height: 1.72; color: var(--ink); }
.pmarketing p, .page-copy p { margin: 0 0 1rem; }
.pmarketing p:last-child, .page-copy p:last-child { margin-bottom: 0; }

.pfeatures, .pgallery { margin: 0 0 2.75rem; }
.pfeatures h2, .pgallery h2 { font-family: var(--font-display); font-weight: 800; font-size: 1.5rem; margin: 0 0 1.25rem; padding-bottom: .5rem; border-bottom: 2px solid var(--red); display: inline-block; }
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; }
.feature { border: 1px solid var(--line); border-radius: 14px; padding: 1.1rem 1.2rem; background: var(--bone); }
.feature h4 { font-family: var(--font-display); font-weight: 800; font-size: 1rem; margin: 0 0 .4rem; }
.feature p { margin: 0; color: var(--muted); font-size: .95rem; line-height: 1.55; }

.gallery-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.1rem; }
/* In the rail the gallery is one column at ~206px, so the auto-fit grid above (which wants 280px a
   track) has to be overridden rather than left to collapse, and the heading comes down to the size
   of a rail heading instead of a section heading. */
.pbody > .pgallery { margin: 0; }
.pbody > .pgallery h2 { font-size: 1.15rem; margin-bottom: .85rem; }
.pbody > .pgallery .gallery-grid { grid-template-columns: minmax(0, 1fr); gap: .9rem; }
.pbody > .pgallery .shot figcaption { font-size: calc(.68rem * var(--accent-scale)); }
.shot { margin: 0; }
.shot img, .shot video { display: block; width: 100%; height: auto; border-radius: 12px; border: 1px solid var(--line); }
/* A video with no poster paints transparent until its first frame decodes, which on the bone page
   background reads as a hole in the grid. --paper is what the product media sits on elsewhere. */
.shot video { background: var(--paper); }
.shot figcaption { font-family: var(--font-mono); font-size: calc(.74rem * var(--accent-scale)); color: var(--muted); margin-top: .5rem; }
.shot-zoom { display: block; width: 100%; padding: 0; border: 0; background: none; cursor: zoom-in; border-radius: 12px; }
.shot-zoom:hover img { border-color: var(--ink); }
.shot-zoom:focus-visible { outline: 2px solid var(--red); outline-offset: 3px; }

/* The gallery lightbox. A <dialog> opened with showModal(), so the browser supplies the backdrop, the
   focus trap and Esc. The image is positioned by transform alone (see the script at the foot of
   Product.aspx): fitted to the stage, a click zooms in around the point clicked, a drag pans. */
.lightbox { position: fixed; inset: 0; width: 100vw; height: 100vh; max-width: none; max-height: none;
            margin: 0; padding: 0; border: 0; background: rgba(20, 18, 17, .94); color: #fff; overflow: hidden; }
.lightbox::backdrop { background: rgba(20, 18, 17, .6); }
.lb-stage { position: absolute; inset: 0; overflow: hidden; touch-action: none; cursor: zoom-in; user-select: none; }
.lb-stage.is-zoomed { cursor: grab; }
.lb-stage.is-dragging { cursor: grabbing; }
.lb-stage img { position: absolute; left: 0; top: 0; max-width: none; transform-origin: 0 0;
                will-change: transform; -webkit-user-drag: none; pointer-events: none; }
.lb-stage.is-animating img { transition: transform .18s ease; }
.lb-close { position: absolute; top: 14px; right: 14px; z-index: 2; width: 44px; height: 44px; border-radius: 999px;
            border: 1px solid rgba(255, 255, 255, .35); background: rgba(20, 18, 17, .7); color: #fff;
            font-size: 1.6rem; line-height: 1; cursor: pointer; }
.lb-close:hover, .lb-close:focus-visible { border-color: #fff; outline: none; }
.lb-nav { position: absolute; top: 50%; transform: translateY(-50%); z-index: 2; width: 48px; height: 48px; border-radius: 999px;
          border: 1px solid rgba(255, 255, 255, .35); background: rgba(20, 18, 17, .7); color: #fff;
          font-size: 2rem; line-height: 1; padding: 0 0 4px; cursor: pointer; }
.lb-nav[hidden] { display: none; }
.lb-prev { left: 14px; }
.lb-next { right: 14px; }
.lb-nav:hover, .lb-nav:focus-visible { border-color: #fff; outline: none; }
.lb-caption { position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%); z-index: 2; max-width: calc(100% - 32px);
              margin: 0; padding: .35rem .8rem; border-radius: 999px; background: rgba(20, 18, 17, .7);
              font-family: var(--font-mono); font-size: calc(.74rem * var(--accent-scale)); pointer-events: none; }
.lb-caption:empty { display: none; }
@media (prefers-reduced-motion: reduce) { .lb-stage.is-animating img { transition: none; } }

.buy-block { scroll-margin-top: 90px; border-top: 1px solid var(--line); padding-top: 2rem; }
.buy-block .buy-h { font-family: var(--font-display); font-weight: 800; font-size: 1.6rem; margin: 0 0 1.25rem; }

/* "In your download" -- what justifies paying over a free generator; sits under the buy heading. */
.deliverables { background: var(--bone); border: 1px solid var(--line); border-radius: 10px; padding: 1.25rem 1.4rem; margin: 0 0 1.75rem; }
.deliverables h3 { font-family: var(--font-mono); text-transform: uppercase; letter-spacing: calc(.08em * var(--accent-track)); font-size: calc(.72rem * var(--accent-scale)); color: var(--ink); margin: 0 0 1rem; }
.dl-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .85rem; }
.dl-list li { display: grid; grid-template-columns: 1.15rem 1fr; column-gap: .65rem; align-items: start; }
.dl-list li::before { content: "\2713"; color: var(--red); font-weight: 800; line-height: 1.4; }
.dl-t { grid-column: 2; font-weight: 700; color: var(--black); }
.dl-d { grid-column: 2; font-size: .9rem; color: var(--ink); opacity: .8; margin-top: .1rem; }

/* Below the width at which .container stops growing there is no 1092px row left to take a remainder
   of, and --side-col collapses toward nothing -- the same reason the home sidebar stacks here. The
   gallery drops below the content it was beside, back to the full-width grid and heading it had
   before, and still lands above Configure & Buy. */
@media (max-width: 1139px) {
    .pbody { grid-template-columns: minmax(0, 1fr); }
    .pbody > .pgallery { margin: 2.75rem 0 0; }
    .pbody > .pgallery h2 { font-size: 1.5rem; }
    .pbody > .pgallery .gallery-grid { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.1rem; }
}

@media (max-width: 720px) {
    .phero { grid-template-columns: 1fr; gap: 1.5rem; }
}

@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    .btn, .btn-outline, .card { transition: none; }
    .hstrip-shot img { transition: none; }
    .hstrip-shot:hover img, .hstrip-shot:focus-visible img { transform: none; }
}

/* ---- Admin area (A6). Staff-only pages: dense, plain, no marketing styling. ---- */
.admin-nav { display: flex; gap: .25rem; border-bottom: 1px solid var(--line); margin: 0 0 1.5rem; }
.admin-tab { padding: .55rem .9rem; text-decoration: none; color: var(--muted);
             border-bottom: 2px solid transparent; font-size: .92rem; }
.admin-tab:hover { color: var(--ink); }
.admin-tab.is-current { color: var(--ink); border-bottom-color: var(--red); }

.admin-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
               gap: 1rem; margin: 0 0 2rem; }
.admin-card { border: 1px solid var(--line); border-radius: 12px; padding: 1rem 1.1rem; background: var(--paper); }
.admin-card h2 { font-size: .82rem; text-transform: uppercase; letter-spacing: .08em;
                 color: var(--muted); margin: 0 0 .4rem; font-weight: 500; }
.admin-card .admin-stat { font-family: var(--font-mono); font-size: calc(1.9rem * var(--accent-scale)); line-height: 1.1; margin: 0 0 .2rem; }
.admin-card .muted { font-size: .84rem; margin: 0; }

.admin-sec { margin: 0 0 2rem; }
.admin-sec h2 { font-size: 1.05rem; margin: 0 0 .75rem; }

/* A row of top-of-page actions on a record: the button, then what it will do, on one line. Wraps
   rather than shrinks on a narrow screen, so the explanation never gets squeezed to one word a column. */
.admin-actions { display: flex; align-items: center; flex-wrap: wrap; gap: .6rem .9rem; margin: 0 0 1.5rem; }
.admin-actions .muted { font-size: .84rem; }

/* Colour dots closing a configuration line, led by the scheme's name. Not admin-* any more: the cart
   closes its lines the same way, so a buyer and an operator read one picture of one configuration.
   The nudge is because .pp-sw is an inline-FLEX box, which otherwise sits its dots on the text
   baseline rather than through the middle of the words. */
.line-sw { margin-left: .35rem; vertical-align: -.12rem; }
.line-scheme { margin-left: .45rem; color: var(--muted); font-size: .82rem; white-space: nowrap; }
.admin-table { width: 100%; border-collapse: collapse; font-size: .9rem; }
.admin-table th { text-align: left; font-weight: 500; color: var(--muted); font-size: .8rem;
                  text-transform: uppercase; letter-spacing: .06em;
                  padding: .5rem .6rem; border-bottom: 1px solid var(--line); }
.admin-table td { padding: .55rem .6rem; border-bottom: 1px solid var(--line); }
.admin-table td.num, .admin-table th.num { text-align: right; font-family: var(--font-mono); }
.admin-table tbody tr:hover { background: var(--bone); }

/* Drag-to-reorder. The grip is the ONLY draggable part of a row -- see admin-reorder.js, which arms
   the row on mousedown here and disarms it on mouseup. */
.admin-table td.grip-cell, .admin-table th.grip-cell { width: 1.9rem; padding-right: 0; text-align: center; }
.grip { background: none; border: 0; padding: .1rem .25rem; cursor: grab; color: var(--muted);
        font-size: 1.05rem; line-height: 1; border-radius: 4px; }
.grip:hover { color: var(--ink); background: var(--bone-2); }
/* A visible focus ring matters more than usual here: the grip is the keyboard route to reordering
   (arrow keys), so it has to be obvious which row the arrows will move. */
.grip:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; color: var(--ink); }
.grip:active { cursor: grabbing; }
.admin-table tbody tr.is-dragging { opacity: .45; background: var(--bone-2); }
.admin-table tbody tr.is-dragging td { border-bottom-color: var(--red); }

/* Order status. Color carries meaning here, so each state is distinct rather than decorative. */
.pill { display: inline-block; padding: .12rem .5rem; border-radius: 999px;
        font-size: .76rem; letter-spacing: .02em; border: 1px solid var(--line); }
.pill-paid { background: #e8f5ec; border-color: #b9dfc6; color: #1d6b36; }
.pill-pending { background: #fdf5e3; border-color: #ecd9a8; color: #8a6416; }
.pill-failed { background: #fdeceb; border-color: #f2c2be; color: #a5231a; }
.pill-refunded { background: var(--bone-2); color: var(--muted); }

.admin-filters { display: flex; gap: .75rem; align-items: flex-end; flex-wrap: wrap; margin: 0 0 1.25rem; }
.admin-filters label { display: flex; flex-direction: column; gap: .25rem; font-size: .82rem; color: var(--muted); }
/* A checkbox filter reads on ONE line with its box, unlike the stacked label-over-control the text and
   select filters use -- so it opts out of the column direction rather than inheriting it. */
.admin-filters label.admin-check { flex-direction: row; align-items: center; gap: .4rem; cursor: pointer; }
.admin-filters label.admin-check input { margin: 0; }
.admin-table th { white-space: nowrap; }
.admin-table tbody th { text-align: left; color: var(--muted); font-weight: 400; width: 12rem; }

/* The refund block is the one destructive control in the admin. Marked out so it never reads as
   just another form on the page. */
.admin-refund { border: 1px solid var(--red); border-radius: 12px; padding: 1rem 1.15rem; }
.admin-refund h2 { color: var(--red-deep); margin-top: 0; }

.notice { padding: .6rem .85rem; border-radius: 8px; margin: 0 0 1.25rem; font-size: .92rem; }
.notice-ok { background: #e8f5ec; border: 1px solid #b9dfc6; color: #1d6b36; }
.notice-bad { background: #fdeceb; border: 1px solid #f2c2be; color: #a5231a; }

/* ---------- The ship queue ----------
   A card per order rather than a table row: an operator working this page needs the whole order in
   one glance -- what to print and where it goes -- and a table wide enough to hold an address puts
   those two columns too far apart to read as one thing. */
.ship-queue { display: grid; gap: 1.25rem; }
.ship-card { border: 1px solid var(--line); border-radius: 12px; padding: 1rem 1.15rem; margin: 0; }
.ship-card h2 { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; }
.ship-card h2 .muted { font-size: .82rem; font-weight: 400; }
.ship-card-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem 1.75rem; }
.ship-h3 { font-size: .72rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted);
           font-weight: 500; margin: 0 0 .35rem; }
.ship-items { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; font-size: .93rem; }
.ship-items .muted { font-size: .84rem; }
.ship-state { margin: .9rem 0 0; font-size: .88rem; display: flex; align-items: center;
              gap: .4rem; flex-wrap: wrap; }
.ship-actions { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-top: .9rem;
                padding-top: .9rem; border-top: 1px solid var(--line); }

/* Where the buyer's own copy of the shipment state goes, under the order header on their history. */
.ship-line { margin: .35rem 0 .6rem; font-size: .9rem; }

@media (max-width: 719px) {
    .ship-card-cols { grid-template-columns: 1fr; }
}

.admin-form { display: grid; gap: .85rem; }
.admin-form label { display: flex; flex-direction: column; gap: .25rem; font-size: .85rem; color: var(--muted); }

/* ---------- Form controls: admin forms, admin filters, and the bare .txt-sm ----------
   One rule for every control on these pages, because the old one matched input[type=text] and
   textarea ONLY. Everything else fell through to the browser's default box: the <select> on
   Suggest, Promotions and the Orders filter; the email field on Suggest (TextMode="Email" renders
   type=email, which input[type=text] does not match); and every .txt-sm, whose box was only ever
   defined as .cfg-grid .txt-sm / .cfg-plate .txt-sm and so reached nothing outside a configurator.
   That is why one row could show a rounded Headline beside a square Promote and square dates.

   Listing the types rather than using a bare `input` on purpose -- a bare selector would also grab
   checkboxes, radios and buttons and give them padding and a border they should not have.

   font-size is set back UP off the label. These labels are small and muted by design; inheriting
   that into the control is what made the boxes feel cramped as much as the .45rem padding did. */
.admin-form input[type=text], .admin-form input[type=email], .admin-form input[type=url],
.admin-form input[type=number], .admin-form input[type=date], .admin-form input[type=search],
.admin-form input[type=password], .admin-form input[type=tel], .admin-form select, .admin-form textarea,
.admin-filters input[type=text], .admin-filters input[type=email], .admin-filters input[type=number],
.admin-filters input[type=date], .admin-filters input[type=search], .admin-filters select,
input.txt-sm {
    font: inherit; font-size: .95rem; color: var(--ink);
    padding: .5rem .6rem;
    border: 1px solid var(--line); border-radius: 8px;
    /* background-COLOR, not the shorthand: the caret rule below paints a background-image, and this
       rule is the later one -- the shorthand would silently reset it and leave a select with no
       arrow at all. Same reason the caret's padding-right is re-stated there rather than here. */
    background-color: var(--paper);
}
.admin-form textarea { line-height: 1.5; resize: vertical; }
/* The filter row is a toolbar, so its controls size to their content instead of filling a column --
   but a bare .txt-sm elsewhere is in a stacked label and should fill it. */
.admin-filters input.txt-sm, .admin-filters select { min-width: 11rem; }
.admin-form input.txt-sm, .admin-form select, .admin-form textarea { width: 100%; }
/* The muted default border is a whisper next to the ink text now sitting in the box; darken it on
   hover and hand focus to the site's red, the way the dialog fields already do. */
.admin-form input:hover, .admin-form select:hover, .admin-form textarea:hover,
.admin-filters input:hover, .admin-filters select:hover, input.txt-sm:hover { border-color: #cfccc6; }
.admin-form input:focus, .admin-form select:focus, .admin-form textarea:focus,
.admin-filters input:focus, .admin-filters select:focus, input.txt-sm:focus {
    outline: 2px solid var(--red); outline-offset: 1px; border-color: var(--red);
}
.admin-form input::placeholder, input.txt-sm::placeholder { color: #a9a59e; }
/* The checkbox opted out of the column layout above; keep it out of the box styling too. */
.admin-filters label.admin-check input { min-width: 0; padding: 0; border: 0; }

/* Drop the OS combo chrome and draw our own caret -- the same treatment .cfg-grid/.ap-grid selects
   already get, so a rounded select stops looking like a Windows control that wandered in. Must sit
   AFTER the padding rule above: it re-states padding-right for the caret's gutter, and a `padding`
   shorthand landing later would close that gutter and print the arrow over the text. */
.admin-form select, .admin-filters select {
    -webkit-appearance: none; -moz-appearance: none; appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none' stroke='%236e6a64' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1.5 1.5 6 6l4.5-4.5'/%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: right .6rem center;
    padding-right: 1.9rem;
}

.admin-form-narrow { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }

/* Stacked field: prompt, then the box, then any note -- in that order, so a note as long as the one
   under "Print & ship FROM" sits below its box instead of shoving it down out of line with the box
   beside it. Subgrid then shares the parent's three rows with every label in the row, so the boxes
   still line up even if a prompt wraps at a narrow column. Labels with no note simply leave the
   third row empty; nothing else moves. */
.admin-form-stack label { display: grid; grid-template-rows: subgrid; grid-row: span 3; gap: .25rem; }
.admin-form-stack .fld-note { font-size: .8rem; line-height: 1.4; color: var(--muted); }
textarea.mono { width: 100%; font-family: var(--font-mono); font-size: calc(.82rem * var(--accent-scale)); line-height: 1.5;
    padding: .55rem; border: 1px solid var(--line); border-radius: 6px; }
/* Marketing links -- the copy-a-tagged-link row under the marketing body (Admin/ProductEdit). One
   line of platform marks, then the selected link in full beside them.
   The URL wraps rather than scrolling inside a nowrap box: a fieldset's default min-width is
   min-content, so one un-wrappable 120-character line would widen the whole Marketing body section
   past the page and put a horizontal scrollbar on the document. */
.mk-head { margin: 1.1rem 0 .1rem; font-size: .95rem; }
.mk-head .muted { font-family: var(--font-body); font-size: .8rem; font-weight: 400; }
.mk-links { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; margin: .5rem 0 0; }
.mk-icons { display: flex; align-items: center; gap: .3rem; flex: 0 0 auto; }
.mk-icon { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px;
    padding: 0; background: none; border: 1px solid transparent; border-radius: 8px;
    color: var(--muted); cursor: pointer; }
.mk-icon:hover { color: var(--ink); background: var(--bone); }
.mk-icon:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }
.mk-icon.is-current { color: var(--red); border-color: var(--line); background: var(--bone); }
/* The five social marks are inline SVG in currentColor, so they take the SAME active/inactive rule the
   four raster marks take below -- muted until this product is actually posted and live there. One rule
   across all nine, or the row has two visual languages and neither reads.

   The profile-link marks (MakerWorld, Thingiverse, Instagram, Facebook) used to be dimmed here as well,
   to say "same link on every product". That dimming is gone: it competed with the posted signal on the
   same channel, and on a greyed mark the two were indistinguishable. The tooltip and the panel say it
   in words instead, which is where a nuance belongs. */
.mk-icon.is-posted { color: var(--ink); }
.mk-icon svg { opacity: .55; }
.mk-icon.is-posted svg, .mk-icon:hover svg, .mk-icon.is-current svg { opacity: 1; }
.mk-out { display: flex; align-items: center; gap: .6rem; min-width: 0; flex: 1 1 240px; }
.mk-url { font-family: var(--font-mono); font-size: calc(.72rem * var(--accent-scale));
    color: var(--ink); background: var(--bone); border: 1px solid var(--line); border-radius: 6px;
    padding: .35rem .5rem; line-height: 1.45; overflow-wrap: anywhere; min-width: 0; flex: 1 1 auto; }
.mk-url-empty { color: var(--muted); background: none; border-color: transparent; padding-left: 0; }
.mk-copied { font-size: .75rem; color: var(--red); white-space: nowrap; flex: 0 0 auto; }
/* The four maker-site brand marks (Content\marketing). Full-colour raster, so unlike the five inline
   SVG glyphs they cannot inherit the button's currentColor -- their state is a filter instead.

   GREYED IS THE DEFAULT and colour is earned: a mark comes up full-colour only where this product is
   actually posted and live on that platform, so the row reads as "these three are up, these six are
   not" before you click anything.

   grayscale ALONE is not enough, and MakerWorld is why: its mark is already black and white, so
   desaturating it changes nothing at all and its inactive state would be indistinguishable from its
   active one. The opacity drop is what carries the signal for every mark regardless of its palette;
   the grayscale is what makes the drop read as "off" rather than "faded". Hover and the selected
   state restore full colour so the control still feels live under the cursor. */
.mk-mark { display: block; width: 20px; height: 20px; object-fit: contain;
    filter: grayscale(1); opacity: .4; transition: opacity .12s ease, filter .12s ease; }
.mk-icon.is-posted .mk-mark { filter: none; opacity: 1; }
.mk-icon:hover .mk-mark, .mk-icon.is-current .mk-mark { filter: none; opacity: 1; }
/* Posted but taken down: colour would say "live". It stays greyed and the dot below says retired. */
.mk-icon.is-retired .mk-mark { filter: grayscale(1); opacity: .55; }
.mk-icon.is-retired:hover .mk-mark, .mk-icon.is-retired.is-current .mk-mark { filter: none; opacity: 1; }
@media (prefers-reduced-motion: reduce) { .mk-mark { transition: none; } }

/* Posted state, read off dbo.TargetProductLinks. A dot rather than a tint on the mark itself: the
   mark's own color already carries "selected", and two meanings on one channel is one too many. */
.mk-icon.is-posted::after, .mk-icon.is-retired::after {
    content: ""; position: absolute; right: 4px; bottom: 4px; width: 7px; height: 7px;
    border-radius: 50%; border: 1.5px solid var(--paper); }
.mk-icon.is-posted::after { background: #2e7d32; }
.mk-icon.is-retired::after { background: var(--muted); }
/* The posting record for whichever mark is selected. One row, revealed by the same click that copies. */
.mk-panel { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
    margin: .6rem 0 0; padding: .55rem .7rem; background: var(--bone); border: 1px solid var(--line);
    border-radius: 8px; font-size: .85rem; }
.mk-panel[hidden] { display: none; }
.mk-posted { display: inline-flex; align-items: center; gap: .35rem; white-space: nowrap; }
.mk-when { color: var(--muted); font-size: .78rem; white-space: nowrap; }
/* "Still live" is only meaningful once something has been posted, so it stays in the form (it must --
   see MarketingLinks.SaveTracking) but out of sight until the Posted box is ticked. */
.mk-live { display: none; align-items: center; gap: .35rem; white-space: nowrap; color: var(--muted); }
.mk-panel.has-posted .mk-live { display: inline-flex; }
.mk-listing { display: flex; align-items: center; gap: .4rem; color: var(--muted);
    flex: 1 1 260px; min-width: 0; }
.mk-listing input { flex: 1 1 auto; min-width: 0; font: inherit; font-size: .8rem; color: var(--ink);
    padding: .3rem .45rem; border: 1px solid var(--line); border-radius: 6px; background: var(--paper); }
.mk-visit { font-size: .78rem; color: var(--muted); white-space: nowrap; }
.mk-visit:hover { color: var(--red); }
.mk-panel .mk-untracked { margin: 0; font-size: .82rem; }

/* ---- Post kit (Admin\PostKit.aspx): the paper checklist as a page ------------------------------
   Everything here is sized for WORKING THROUGH, not for reading: the steps are a column you tick
   down, and each pasteable field puts its copy button on the same line as its label so the button is
   never hunting distance from the thing it copies. */
/* A done step stays fully readable -- greying it out would hide the thing you tick to undo a mistake.
   The strike is on the label alone, so the hint under it can still be read. */
.pk-step.is-done .pk-step-label { text-decoration: line-through; color: var(--muted); font-weight: 400; }
.pk-step-hint { display: block; margin-left: 1.4rem; font-size: .8rem; color: var(--muted); }
.pk-progress { margin: .75rem 0 0; font-size: .82rem; }

.pk-field { margin: 0 0 1.1rem; }
.pk-field:last-child { margin-bottom: 0; }
.pk-field-head { display: flex; align-items: center; justify-content: space-between; gap: .75rem;
    margin: 0 0 .25rem; }
.pk-field-head label { font-size: .85rem; color: var(--muted); font-weight: 600; }
.pk-hint { margin: .25rem 0 0; font-size: .78rem; }
.pk-copy, .pk-copyimg { font: inherit; font-size: .75rem; padding: .2rem .6rem; cursor: pointer;
    color: var(--muted); background: var(--paper); border: 1px solid var(--line); border-radius: 999px; }
.pk-copy:hover, .pk-copyimg:hover { color: var(--ink); border-color: var(--muted); }
.pk-copy.is-done, .pk-copyimg.is-done { color: var(--paper); background: #2e7d32; border-color: #2e7d32; }
.pk-block { margin: 0; padding: .6rem .7rem; background: var(--bone); border: 1px solid var(--line);
    border-radius: 6px; font-family: var(--font-mono); font-size: calc(.78rem * var(--accent-scale));
    line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }

.pk-links td { vertical-align: middle; }
.pk-url { font-family: var(--font-mono); font-size: calc(.7rem * var(--accent-scale));
    overflow-wrap: anywhere; }

.pk-images { display: flex; flex-wrap: wrap; gap: .75rem; margin: 0 0 .6rem; }
.pk-img { margin: 0; width: 150px; }
/* grab, not pointer: the picture's primary gesture here is the drag, and the cursor should say so. */
.pk-img .pk-drag { display: block; cursor: grab; border: 1px solid var(--line); border-radius: 8px;
    overflow: hidden; background: var(--bone); }
.pk-img .pk-drag:active { cursor: grabbing; }
.pk-img img { display: block; width: 100%; height: 110px; object-fit: contain; }
.pk-img figcaption { display: flex; align-items: center; justify-content: space-between; gap: .4rem;
    margin: .3rem 0 0; }
.pk-img-kind { font-size: .72rem; color: var(--muted); text-transform: uppercase;
    letter-spacing: .06em; }

.admin-thumb { width: 120px; height: auto; border: 1px solid var(--line); border-radius: 6px; }
video.admin-thumb { display: block; background: var(--paper); }
.admin-wide { width: 100%; font: inherit; padding: .4rem .5rem; border: 1px solid var(--line); border-radius: 6px; }
.btn-link { background: none; border: 0; padding: 0; color: var(--muted); cursor: pointer;
    font: inherit; text-decoration: underline; }
.btn-link:hover { color: var(--red); }

.admin-scroll { overflow-x: auto; margin: .5rem 0 1.25rem; }
.admin-sec h3 { margin: 1.1rem 0 .25rem; }
.admin-sec h3 + .muted { margin: 0 0 .5rem; font-size: .82rem; }

.hp-field { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
.admin-subnav { margin-top: -.4rem; font-size: .85rem; }
.sug-card { border: 1px solid var(--line); border-radius: 10px; padding: .9rem 1rem; margin: 0 0 .9rem; }
.sug-meta { margin: 0 0 .3rem; font-size: .82rem; color: var(--muted); }
.sug-body { margin: .5rem 0 .7rem; white-space: normal; }
.sug-actions { margin: .6rem 0 0; display: flex; gap: 1rem; flex-wrap: wrap; }

/* "Post a make", on an order line. The <li> it sits in is a flex row that already wraps, so this
   claims a whole row of its own rather than squeezing in beside the name and the download buttons. */
.make-post { flex: 1 1 100%; margin: .35rem 0 .1rem; }
.make-post > summary { cursor: pointer; font-size: .88rem; color: var(--muted); width: fit-content; }
.make-post > summary:hover { color: var(--red); }
.make-post[open] > summary { color: var(--ink); font-weight: 600; }
.make-post-body { margin: .75rem 0 .3rem; padding: .9rem 1rem 1rem; border: 1px solid var(--line); border-left: 3px solid var(--red); border-radius: 10px; background: var(--bone); }
.make-post-body .admin-form { gap: .7rem; }
.make-post-body textarea { width: 100%; max-width: 34rem; }
.make-post-body .cfg-note { margin: .8rem 0 .9rem; max-width: 62ch; }
.make-post-body p:last-child { margin-bottom: 0; }

.make-card { display: flex; gap: 1rem; align-items: flex-start; }
.make-thumb { width: 190px; height: auto; border: 1px solid var(--line); border-radius: 8px; display: block; }
.make-meta { flex: 1; min-width: 0; }

.cat-nav { display: flex; gap: .9rem; flex-wrap: wrap; margin: -.4rem 0 1.4rem; font-size: .88rem; }
.cat-nav a { color: var(--muted); text-decoration: none; border-bottom: 1px solid var(--line); }
.cat-nav a:hover { color: var(--red); border-color: var(--red); }
.cat-pick { display: flex; gap: .8rem 1.4rem; flex-wrap: wrap; }
.cat-pick label { display: flex; gap: .35rem; align-items: center; font-size: .9rem; }

/* ---------- Category shelves (home catalog) ----------
   One horizontally scrolling track per category. The track is a plain overflow container, so swipe,
   trackpad, shift-wheel and the scrollbar all work with no script; the arrows are layered on top. */
/* The catalog's table of contents: in-page links to each shelf. */
.shelf-jump { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0 0 2rem; }
.shelf-jump a {
    font-family: var(--font-mono); font-size: calc(.74rem * var(--accent-scale)); text-transform: uppercase; letter-spacing: calc(.05em * var(--accent-track));
    color: var(--muted); text-decoration: none;
    border: 1px solid var(--line); border-radius: 999px; padding: .38rem .85rem;
}
.shelf-jump a:hover { color: var(--ink); border-color: var(--ink); }
.shelf-jump a:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }

/* scroll-margin, because the header is sticky: without it a jump puts the shelf heading UNDER the
   header and the visitor lands looking at cards with no idea which shelf they are on. */
.shelf { margin: 0 0 2.6rem; scroll-margin-top: 88px; }
.shelf-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin: 0 0 .85rem; }
.shelf-head h3 { font-weight: 800; font-size: 1.22rem; }
.shelf-all { font-size: .85rem; color: var(--muted); text-decoration: none; white-space: nowrap; border-bottom: 1px solid var(--line); }
.shelf-all:hover { color: var(--red); border-color: var(--red); }

.shelf-body { position: relative; }
.shelf-scroll {
    overflow-x: auto; overflow-y: hidden;
    scroll-snap-type: x proximity; scroll-behavior: smooth;
    scrollbar-width: thin;
    /* The cards lift on hover, and an overflow container clips that. Pad, then pull the padding back
       out with a matching negative margin, so the lift has room without the shelf gaining a gutter. */
    padding: 5px 5px 14px; margin: -5px -5px -14px;
}
/* The shelf card's width and gutter, hoisted out of the rule below because the home sidebar sizes
   ITSELF from them -- it takes whatever the row has left after three of these fit (see .home-body).
   Declared here, beside the rule that draws the card, so the two cannot drift apart. */
:root {
    --shelf-card: 270px; --shelf-gap: 1.25rem;
    /* The gutter between the catalog and the sidebar, and between the product hero and its gallery
       rail. Fixed, not viewport-relative -- see .home-body. */
    --side-gap: 2rem;
    /* THE SIDE COLUMN. Whatever a 1092px content row has left once three shelf cards, their gutters
       and the side gutter are paid for: 206px. The home page's Suggestion box column and the product
       page's gallery rail are both this, so the two pages line up rather than merely look similar.
       The percentage resolves against whichever row uses it, and both rows are the container's
       1092px. The +4px is so a sub-pixel container width cannot shave the third card. */
    --side-col: calc(100% - (3 * var(--shelf-card) + 2 * var(--shelf-gap) + 4px) - var(--side-gap));
}

.shelf-track {
    display: grid; grid-auto-flow: column; grid-auto-columns: var(--shelf-card); gap: var(--shelf-gap);
    /* One fixed width, not a minmax. A range lets each shelf size its cards to its own item count --
       250px on a shelf of seven, 290px on a shelf of two -- so nothing lines up down the page. Fixed
       also leaves a card part-visible at the edge on most widths, which says "this scrolls" better
       than any arrow does. justify-content keeps a short shelf left-aligned instead of stretched. */
    justify-content: start;
}
.shelf-track .card { scroll-snap-align: start; }
/* Clamp the blurb, so one long tagline does not set the height of every card on the shelf. The clamp
   goes on the inner span, not the <p>: see Services\Catalog.vb -- the <p> is a flex item and its
   display gets blockified out from under -webkit-box. min-height keeps short and long cards level. */
.shelf-track .card .desc { min-height: 3.9em; }
.shelf-track .card .desc > span {
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
/* Long names wrap to two lines on a narrow card; reserve both so the price rows line up shelf-wide. */
.shelf-track .card h3 { font-size: 1.08rem; min-height: 2.5em; }
.shelf-track .card .meta { flex-wrap: wrap; }

.shelf-nav {
    position: absolute; top: 50%; transform: translateY(-50%);
    width: 36px; height: 36px; padding: 0; border-radius: 50%;
    border: 1px solid var(--line); background: rgba(255, 255, 255, .94);
    color: var(--ink); font-size: 1.35rem; line-height: 1; cursor: pointer;
    display: grid; place-items: center;
    box-shadow: 0 4px 16px rgba(13, 12, 11, .12);
    transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.shelf-nav:hover { background: #fff; border-color: var(--red); color: var(--red); }
.shelf-nav[hidden] { display: none; }
.shelf-prev { left: -10px; }
.shelf-next { right: -10px; }
@media (max-width: 860px) {
    /* Touch scrolls directly; the arrows would only cover a card. */
    .shelf-nav { display: none; }
    /* One width, not a minmax: a max is an invitation to stretch, and a two-card shelf took it --
       the pair split the viewport and came out half again as wide as a full shelf's cards. */
    .shelf-track { grid-auto-columns: min(78vw, 300px); }
}

/* ---- Home gallery strip ----
   The band of product shots under the hero: six across, twelve deep, drifting sideways on its own
   and wrapping to the first after the twelfth (the wrap is in Default.aspx). No heading and no
   captions -- it is a shop window, not a section of the page -- but every shot links to its
   product, so it is not hidden from assistive technology. */
.hstrip {
    --hstrip-across: 6;
    --hstrip-gap: 1rem;
    overflow-x: auto; overflow-y: hidden;
    /* The script writes scrollLeft every frame; a smooth scroll-behavior would have the browser
       animating toward each of those in turn and the band would crawl or stall. */
    scroll-behavior: auto;
    /* No scrollbar: it moves by itself, so a bar tracking back and forth under it is a distraction
       rather than an affordance. Dragging, swiping and the wheel all still work. */
    scrollbar-width: none; -ms-overflow-style: none;
    /* The shots lift a little on hover and overflow-y: hidden would shave the top and bottom off the
       one being pointed at. Pad, then pull the padding back out with a matching negative margin, so
       the lift has room without the band gaining a gutter -- the same trick as .shelf-scroll. Block
       only: an inline padding would come off the width the six shots are measured against. */
    padding-block: 5px;
    /* NO margin of its own, top or bottom. The hero above ends in a 4rem padding-block and the
       section head below opens with a 4rem margin -- each is the separation those elements have
       always had from their neighbors, and adding the band's own on top of either put nearly 100px
       of empty page around the photographs. The -5px is just the padding above cancelled back out,
       so the gaps those two set are the gaps you see. */
    margin: -5px 0;
}
.hstrip::-webkit-scrollbar { display: none; }
.hstrip-track {
    display: grid; grid-auto-flow: column;
    /* Six FILL the row exactly -- this is a band, not a shelf, so nothing is left half-showing at
       the edge to say "this scrolls". It says that by moving. */
    grid-auto-columns: calc((100% - (var(--hstrip-across) - 1) * var(--hstrip-gap)) / var(--hstrip-across));
    gap: var(--hstrip-gap);
}
/* Each shot is a link to the product it is a photo of. The band pauses on hover, so it is a target
   that holds still once someone reaches for it. */
.hstrip-shot { display: block; border-radius: 12px; }
.hstrip-shot:focus-visible { outline: 2px solid var(--red); outline-offset: 3px; }
.hstrip-shot img { transition: transform .18s ease, border-color .18s ease; }
.hstrip-shot:hover img, .hstrip-shot:focus-visible img { transform: scale(1.03); border-color: var(--red); }

/* A fixed 4:3 window on every shot, whatever shape the file is: gallery images are a mix of square
   heroes and wider in-use photos, and a strip of ragged heights reads as a mistake. */
.hstrip-shot img {
    display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
    border-radius: 12px; border: 1px solid var(--line); background: var(--paper);
}
/* Fewer, larger shots as the row narrows -- six across a phone is six thumbnails nobody can make
   anything out in. */
@media (max-width: 860px) { .hstrip { --hstrip-across: 4; --hstrip-gap: .75rem; } }
@media (max-width: 560px) { .hstrip { --hstrip-across: 3; } }

/* ---------- Home sidebar ----------
   Beside the shelves, not above or below them. minmax(0, 1fr) on the catalog column is load-bearing:
   the shelves are overflow-x scrollers, and an `auto`/`1fr` track measures their UNSCROLLED content --
   a shelf of nine cards would size the column to ~2500px and shove the sidebar off the page. */
.home-body {
    display: grid;
    /* The sidebar is the REMAINDER, not a width of its own -- see --side-col in :root. Sizing it that
       way is what keeps the third card whole; a fixed sidebar width silently eats into the shelf
       instead (a fixed 20rem left the catalog at 737px -- two cards and a sliver of a third).
       Both tracks are minmax(0, ...) so neither can be forced wider than the row by its contents. */
    grid-template-columns: minmax(0, 1fr) minmax(0, var(--side-col));
    /* Fixed, not the old clamp(..., 3vw, ...): the container is capped at 1140px, so a
       viewport-relative gutter only made the catalog column breathe in and out by a few px --
       enough to put the third card's edge on and off the screen depending on the window. */
    gap: var(--side-gap);
    /* start, not stretch: the catalog runs ~2600px and the aside ~1000px, and a stretched aside would
       hand that whole height to its last card rather than letting the pair sit at the top of the row. */
    align-items: start;
}
/* Stack below the width at which .container stops growing. Above it the row is a known 1092px and
   the three-card arithmetic is exact; below it the row shrinks but the three cards do not, so the
   remainder the sidebar lives on would collapse to a useless 14px sliver before it ever stacked.
   The sidebar falls below the catalog, which is the right order -- the models are what the page
   is for -- and gets the full width back to read in. */
@media (max-width: 1139px) { .home-body { grid-template-columns: minmax(0, 1fr); } }

/* NOT sticky. It was, at top: 88px, and that only works while the aside is shorter than the viewport.
   At 206px the same copy sets ~1014px tall, and a stuck element taller than the window never scrolls
   up again -- the bottom 200px of the AI card became permanently unreachable on a 900px screen. The
   height moves every time the copy changes, so this cannot be fixed with a height threshold; scrolling
   with the page has no such failure mode. */
.home-side { display: grid; gap: 1.1rem; }

/* Tighter than a card elsewhere on the site, because at 206px the padding is the difference between
   a readable measure and four words a line. */
.side-card { border: 1px solid var(--line); border-radius: 14px; padding: 1rem 1.05rem 1.1rem; background: var(--bone); }
.side-card h3 { font-weight: 800; font-size: 1.02rem; line-height: 1.25; margin: .35rem 0 .5rem; }
.side-card p { margin: 0 0 .7rem; color: var(--muted); font-size: .88rem; line-height: 1.55; }
.side-card p:last-child { margin-bottom: 0; }
/* Full width and centered: at this measure the label wraps to two lines, and a wrapped button that
   still fills its column reads as deliberate where a shrink-wrapped one reads as broken. */
.side-card .side-btn { display: flex; justify-content: center; text-align: center; margin-top: .5rem; font-size: .85rem; padding: .5rem .8rem; }

/* Stacked, the sidebar has the whole page width -- so the two cards sit side by side rather than
   as one long ribbon, and the type goes back to its comfortable size. */
@media (max-width: 1139px) {
    .home-side { grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); gap: 1.25rem; margin-top: 2.5rem; }
    .side-card { padding: 1.15rem 1.25rem 1.3rem; }
    .side-card h3 { font-size: 1.12rem; }
    .side-card p { font-size: .93rem; line-height: 1.6; }
    .side-card .side-btn { display: inline-flex; }
}

.promo-band { margin: 0 0 2rem; }
.promo-card { border-color: var(--red); }
.promo-flag { margin: 0 0 .2rem; font-size: .68rem; letter-spacing: .1em; text-transform: uppercase; color: var(--red); font-weight: 500; }

/* ---------- The posting wizard (Admin\PostKit) ----------
   One step on screen at a time, and narrow on purpose: this page is worked in half a monitor with the
   target site's upload form in the other half, so nothing here may need horizontal room. */
.pk-hero h1 { margin-bottom: .15rem; }
.pk-rail { display: flex; flex-wrap: wrap; gap: .3rem; list-style: none; margin: 0 0 1.25rem; padding: 0;
    counter-reset: pk; }
.pk-rail-item { counter-increment: pk; }
.pk-rail-item a { display: inline-flex; align-items: center; gap: .4rem; padding: .3rem .6rem;
    border: 1px solid var(--line); border-radius: 999px; font-size: .78rem; color: var(--muted);
    text-decoration: none; white-space: nowrap; }
.pk-rail-item a::before { content: counter(pk); font-family: var(--font-mono); font-size: .7rem;
    color: var(--muted); }
.pk-rail-item a:hover { border-color: var(--ink); color: var(--ink); }
/* Done is stated with a tick as well as colour -- the rail is scanned, not read. */
.pk-rail-item.is-done a { color: #2e7d32; border-color: #cfe6cf; background: #f2f8f2; }
.pk-rail-item.is-done a::before { content: "\2713"; color: #2e7d32; }
.pk-rail-item.is-current a { color: var(--paper); background: var(--ink); border-color: var(--ink);
    font-weight: 600; }
.pk-rail-item.is-current a::before { color: var(--paper); }

.pk-step-body { border: 1px solid var(--line); border-radius: 12px; padding: 1.1rem 1.25rem 1.35rem; }
.pk-step-title { font-size: 1.15rem; margin: 0 0 .2rem; }
.pk-step-hint { margin: 0 0 1.1rem; font-size: .88rem; }
.pk-actions { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin: 1rem 0 0; }
.pk-actions .btn-link { margin-left: auto; }
.pk-parked { display: block; }
.pk-do { margin: 0 0 1rem; padding-left: 1.2rem; }
.pk-do li { margin: .2rem 0; }

/* The drop target wraps a real file input, so a drag works and the picker still does. Shared by
   the post kit and the product editor -- see Content/admin-drop.js. The input is hidden rather
   than merely styled, which is why the label has to say what is staged: there is no "no file
   chosen" text left to read. A staged zone goes solid, so "ready" reads differently from "empty"
   at a glance and not only by its wording. */
.drop-zone { display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: .3rem; min-height: 120px; padding: 1.25rem; margin: 0 0 1rem; cursor: pointer;
    border: 2px dashed var(--line); border-radius: 12px; background: var(--bone);
    transition: border-color .12s ease, background .12s ease; }
.drop-zone:hover, .drop-zone.is-over { border-color: var(--red); background: #fdf6f6; }
.drop-zone.is-loaded { border-style: solid; border-color: var(--red); background: #fdf6f6; }
.drop-zone input[type=file] { position: absolute; width: 1px; height: 1px; opacity: 0; }
.drop-zone-label { font-weight: 600; word-break: break-all; }
.drop-zone-sub { font-size: .8rem; }

.pk-facts { display: flex; flex-wrap: wrap; gap: .4rem .9rem; margin: 0 0 .75rem; font-size: .85rem;
    color: var(--muted); }
.pk-facts strong { color: var(--ink); }
.pk-files { list-style: none; margin: 0 0 1rem; padding: 0; display: flex; flex-wrap: wrap; gap: .3rem; }
.pk-files a { display: inline-block; padding: .2rem .5rem; border: 1px solid var(--line);
    border-radius: 6px; font-family: var(--font-mono); font-size: .72rem; text-decoration: none; }
.pk-files a:hover { border-color: var(--ink); }

/* Hero: the viewer is SQUARE, matching the render's own aspect. A 16:9 picker would let you compose
   an angle that the square auto-framer then crops differently. */
.pk-hero-grid { display: grid; grid-template-columns: minmax(260px, 1fr) minmax(240px, 1fr);
    gap: 1.25rem; align-items: start; }
.pk-viewer { aspect-ratio: 1 / 1; width: 100%; border: 1px solid var(--line); border-radius: 10px;
    overflow: hidden; background: var(--bone); }
.pk-viewer canvas { display: block; width: 100% !important; height: 100% !important; }
.pk-hero-img { width: 100%; max-width: 240px; border: 1px solid var(--line); border-radius: 10px;
    display: block; margin: 0 0 .75rem; }
.pk-hero-existing { margin: 0 0 1rem; padding: .75rem; border: 1px solid var(--line);
    border-radius: 10px; background: var(--bone); }
.pk-hero-existing p { margin: 0 0 .5rem; }
.pk-inline { display: inline-flex; align-items: center; gap: .4rem; font-size: .85rem; color: var(--muted); }
.pk-confirm { margin: 1rem 0 .5rem; font-weight: 600; }
.pk-colors { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: .3rem; }
.pk-colors li { display: flex; align-items: center; gap: .5rem; font-size: .9rem; }

.pk-plates .pk-plate-head td { background: var(--bone); font-weight: 600; font-size: .8rem; }
.pk-plate-thumb { width: 46px; height: 46px; object-fit: contain; display: block; }

.pk-field { margin: 0 0 1rem; }
.pk-sheet-head { display: flex; align-items: center; justify-content: space-between; gap: .6rem;
    margin: 0 0 .3rem; }
.pk-sheet-label { font-size: .75rem; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
.pk-copy { padding: .15rem .6rem; font-size: .75rem; cursor: pointer; }
.pk-copy.is-copied { color: #2e7d32; border-color: #cfe6cf; }
.pk-pre { font-family: var(--font-mono); font-size: .78rem; line-height: 1.5; white-space: pre-wrap;
    overflow-wrap: anywhere; background: var(--bone); border: 1px solid var(--line); border-radius: 8px;
    padding: .6rem .7rem; margin: 0; }
.pk-one { display: block; font-family: var(--font-mono); font-size: .76rem; background: var(--bone);
    border: 1px solid var(--line); border-radius: 8px; padding: .45rem .6rem; overflow-wrap: anywhere; }
.pk-ref { margin: 1rem 0 0; font-size: .85rem; }
.pk-ref summary { cursor: pointer; color: var(--muted); }

/* The per-site sheet. Copying a field marks it done and points at the next one, so an interrupted
   session resumes by looking rather than by remembering. */
.pk-sitetabs { display: flex; flex-wrap: wrap; gap: .3rem; margin: 0 0 1rem; }
.pk-sitetab { padding: .3rem .65rem; border: 1px solid var(--line); border-radius: 999px;
    font-size: .8rem; text-decoration: none; color: var(--muted); }
.pk-sitetab.is-posted { border-color: #cfe6cf; background: #f2f8f2; color: #2e7d32; }
.pk-sitetab.is-current { background: var(--ink); border-color: var(--ink); color: var(--paper); font-weight: 600; }
.pk-sheet { list-style: none; margin: 0 0 1rem; padding: 0; counter-reset: f; }
.pk-sheet-row { counter-increment: f; padding: .6rem .7rem; margin: 0 0 .5rem; border-radius: 10px;
    border: 1px solid transparent; }
.pk-sheet-row .pk-sheet-label::before { content: counter(f) ". "; font-family: var(--font-mono); }
.pk-sheet-row.is-done { opacity: .55; }
.pk-sheet-row.is-next { border-color: var(--red); background: #fdf6f6; }
.pk-sheet-note { margin: .35rem 0 0; font-size: .78rem; }

.pk-qr { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin: 1rem 0;
    padding: .9rem; border: 1px solid var(--line); border-radius: 12px; }
.pk-qr canvas { display: block; }
.pk-strip { display: flex; flex-wrap: wrap; gap: .6rem; margin: .5rem 0 0; }
.pk-shot { margin: 0; width: 130px; }
.pk-shot img, .pk-shot video { width: 130px; height: 130px; object-fit: cover; border: 1px solid var(--line);
    border-radius: 8px; display: block; }
.pk-shot figcaption { font-size: .72rem; color: var(--muted); margin: .25rem 0 .1rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

@media (max-width: 900px) {
    .pk-hero-grid { grid-template-columns: 1fr; }
}

/* ---------- Staff price override (Checkout) ----------
   Deliberately looks like a control panel and not like part of the shop: a red-edged inset that a
   buyer never sees, so a comp can never be mistaken for a promotion the customer is being offered. */
.staff-free { border: 1px solid var(--red); border-left-width: 4px; border-radius: 8px;
    background: #fdf6f6; padding: .75rem .9rem; margin: 0 0 1rem; }
.staff-free-row { display: flex; align-items: flex-start; gap: .55rem; font-size: .9rem; cursor: pointer; }
.staff-free-row input { margin-top: .25rem; }
.staff-free-row .muted { font-size: .82rem; }
.staff-free-why { margin: .6rem 0 0; }
.staff-free-why label { display: block; font-size: .8rem; color: var(--muted); margin: 0 0 .2rem; }

/* The hero render blocks for the best part of a minute, so the button says so while it works. It is
   never disabled: disabling a submit button drops its name from the post, and pk_render is the field
   the server dispatches on. */
.pk-rendering { display: block; margin: .5rem 0 0; font-size: .85rem; color: var(--red); }
button.is-busy { opacity: .75; cursor: progress; }

/* The way into the posting wizard, at the foot of the marketing section -- next to the tagged links,
   because those links are what the wizard exists to publish. */
.pk-launch { display: flex; align-items: center; gap: .7rem; flex-wrap: wrap; margin: 1rem 0 0; }
.pk-launch .btn-outline { text-decoration: none; }

/* Shown only if the QR library never arrives -- the step stays usable as a plain link. */
.pk-qr-fallback { display: block; font-family: var(--font-mono); font-size: .75rem; overflow-wrap: anywhere; }
