@font-face {
  font-family: "Anton";
  src: url("/anton-subset.ttf") format("truetype");
  font-display: swap;
  font-weight: 400;
  font-style: normal;
}

:root { --bg:#fff; --fg:#000;
  /* ONE content column. The photo, the statement, the slider, the number field,
     the size bars, the buy button, the disclosure and the footer list all sit
     in it and agree on both edges at every viewport. Nothing below the header
     sets its own width or its own horizontal margin.

     Width is set by the STATEMENT, which is the widest fixed thing on the
     page and the one that must not wrap. At the clamp ceiling of 1.5rem,
     "I PAID $99,999.99 FOR THIS SHIRT" — the longest string the preview
     ceiling permits — measures 336px, and $5,000.00 measures 324px. 20rem
     (320px) missed both, orphaning "SHIRT" onto its own line. 21.5rem clears
     the longest of them with a little room for a fallback font.
     Below ~545px the statement's 4.4vw sizing takes over and shrinks the text
     faster than the column narrows, so phones were never the problem. */
  --col:21.5rem; --gutter:1rem;
  /* Derived, not restated: these two drifting apart is a whole-page bug. */
  --col-box:calc(var(--col) + 2 * var(--gutter)); }
* { box-sizing: border-box; }
body { margin:0; background:var(--bg); color:var(--fg);
       font:16px/1.4 -apple-system, "Helvetica Neue", Arial, sans-serif;
       font-stretch:condensed; }
a { color:var(--fg); }
header { padding:1rem; font-size:.9rem; text-transform:lowercase; }
main { max-width:40rem; margin:0 auto; padding:0 1rem 1rem; }
/* The shop pages (index, success). Policy pages keep the wider prose measure,
   so this is opt-in rather than a change to `main` itself. */
main.col { max-width:var(--col-box); padding:0 var(--gutter) 1rem; }
/* ONE garment view. #mockup is a real product photograph from the printer;
   #artwork is the print file alone inside a scale drawing of the print area,
   shown only when no photograph can be produced.
   No t-shirt is drawn anywhere in this stylesheet. */
#preview { margin:0; max-width:none; }
/* Both states occupy the same fixed-aspect box, and the box is sized before any
   image loads, so the frame never empties, never reflows, and the photo swap
   (a decoded image assigned to an already-laid-out element) shifts nothing. */
/* clip, because #mockup is deliberately wider than the column (see below) and
   the overhang must not widen the page on a phone. It only ever cuts the
   photograph's empty margin: at the scale below the garment sits inside the
   frame on all four sides. */
#preview-frame { position:relative; width:100%; aspect-ratio:1 / 1;
  overflow:clip; }
/* The frame is FROZEN, not blanked, while the next photograph decodes: it keeps
   the last good picture rather than flipping to line art, which on a page about
   one garment reads as the product glitching. Dimmed for exactly that window so
   a photograph one price behind never passes for a current one. Same device and
   the same reasoning as #breakdown.stale. The buy button is deliberately NOT
   gated on this — see garmentView() in public/state.js. */
#preview-frame.stale { opacity:.55; }
#preview-frame { transition:opacity 120ms ease-out; }
@media (prefers-reduced-motion: reduce) { #preview-frame { transition:none; } }
.reduced-motion #preview-frame { transition:none; }
#mockup { position:absolute; inset:0; width:100%; height:100%; object-fit:contain;
  /* The photograph is 1200x1200 with the garment centred and 982px wide, so
     shown at its own size the sleeve tips stop 9% short of the column while
     the FILE's edges sit on it — the picture lined up, not the shirt. Scaling
     by the ratio of the two puts the sleeve tips on the column edges instead,
     which is where the statement, the size bars and the buy button all start
     and end. Measure the garment again if the printer's photograph changes;
     these are its pixels, not a taste. */
  scale:calc(1200 / 982); }
[hidden] { display:none !important; }
#artwork { position:absolute; inset:0; display:flex; align-items:center;
  justify-content:center; border:1px solid #000; }
/* aspect-ratio is set per size by public/app.js from the real print area
   dimensions served by /api/print-geometry. Never hardcode it here. */
#print-area { position:relative; height:94%; outline:1px dashed #bbb; }
/* left/top/width are set per frame by app.js from the placement fractions that
   are sent to Printify; translate(-50%,-50%) makes them centre coordinates,
   matching CHEST_X/CHEST_Y. */
#art { position:absolute; transform:translate(-50%,-50%); }
#art svg { display:block; width:100%; height:auto; }
#statement { text-align:left; font-size:clamp(1rem,4.4vw,1.5rem); font-weight:700;
  letter-spacing:.02em; margin:.25rem 0 0; overflow-wrap:break-word;
  /* If it ever does wrap — a fallback font, a longer string than the preview
     ceiling allows — split it into even lines rather than stranding one word.
     A lone "SHIRT" on line two is the specific ugliness this avoids. */
  text-wrap:balance; }
form { display:grid; gap:.75rem; max-width:none; margin:1rem 0 0; }
label { display:grid; gap:.25rem; font-size:.85rem; }
/* --- the segmented price slider ------------------------------------------
   A custom control, because travel is allocated by expected use rather than by
   value (see src/slider.ts). Painted as a track, a fill and a thumb; the
   geometry all comes from app.js as percentages. */
#price-label { font-size:.85rem; }
/* --pos is the track position, 0..1, written by app.js. The thumb travels
   between its own half-widths rather than between the track's edges, so at 0
   and 1 it sits fully inside the control instead of hanging half off it. Every
   position below is expressed against that inset travel, which is what keeps
   the fill, the thumb and the segment ticks agreeing at the ends. */
#price-track { position:relative; height:2.2rem; cursor:pointer;
  touch-action:none;                 /* the control owns horizontal drags */
  --track-h:4px; --thumb:1.15rem; --pos:0;
  --travel:calc(var(--pos) * (100% - var(--thumb)) + var(--thumb) / 2); }
#price-track::before { content:""; position:absolute;
  left:calc(var(--thumb) / 2); right:calc(var(--thumb) / 2);
  top:calc(50% - var(--track-h) / 2); height:var(--track-h); background:#e2e2e2; }
#price-fill { position:absolute; left:calc(var(--thumb) / 2);
  top:calc(50% - var(--track-h) / 2); height:var(--track-h); background:#000;
  width:calc(var(--travel) - var(--thumb) / 2); }
#price-thumb { position:absolute; top:50%; left:var(--travel);
  width:var(--thumb); height:var(--thumb);
  margin:0; transform:translate(-50%,-50%); border-radius:50%; background:#000; }
/* A price off the track (below the floor, or past the slider's right end) has
   no position on it. The thumb still parks at the nearest end, so it is hollowed
   out rather than left claiming to represent the value. */
#price-track.off-track #price-thumb { background:var(--bg); box-shadow:inset 0 0 0 2px #000; }
#price-track.off-track #price-fill { background:#bbb; }
#price-track:focus-visible { outline:2px solid #000; outline-offset:3px; }
/* Segment boundaries. A hairline and nothing else: the change in sensitivity is
   worth seeing coming, and is not worth copy on a page that has almost none. */
#price-ticks { position:absolute; inset:0; pointer-events:none; }
.price-tick { position:absolute; top:50%; width:1px; height:.7rem;
  transform:translate(-50%,-50%); background:#bbb;
  /* Same inset travel as the thumb, so a boundary tick sits exactly where the
     thumb lands when it is on that boundary. */
  left:calc(var(--tick-pos) * (100% - var(--thumb)) + var(--thumb) / 2); }
/* First-paint positions for the two ticks shipped in the markup ($50 and $250,
   the SLIDER_SEGMENTS boundaries). In CSS rather than style= attributes so the
   pages carry no inline styles and the CSP needs no 'unsafe-inline' for
   style-src. renderTicks() replaces these elements and positions them through
   el.style.setProperty, which CSP does not govern, and an element-level custom
   property always beats these selector defaults. */
.price-tick:nth-child(1) { --tick-pos:0.5; }
.price-tick:nth-child(2) { --tick-pos:0.75; }

/* The price, typed. The statement line is the only place a price is entered,
   so the number carries an underline to read as editable while the sentence
   around it stays plain text. */
#price-input { cursor:text; border-bottom:2px solid currentColor;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
  white-space:pre; }
#price-input:focus { outline:none; background:#f2f2f2; }

/* Free shipping, and the reason a price cannot be bought. Both are single
   lines under the button; #price-note is empty in the ordinary case. */
#free-shipping { margin:.15rem 0 0; font-size:.8rem; }
#price-note { margin:.15rem 0 0; font-size:.8rem; }

/* These lines are empty in the ordinary case. Collapsed rather than merely
   blank, so the form's grid gap does not reserve a row for something that is
   saying nothing. */
#price-note:empty, #error:empty, #free-shipping[hidden] { display:none; }

/* --- the inquire form ----------------------------------------------------
   Shown only above the card cap. Takes a contact address and a note; charges
   nothing. */
#inquire { display:grid; gap:.5rem; margin:1rem 0 0; }
#inquire-lead { margin:0; font-size:.85rem; }
#inquire textarea, #inquire input { width:100%; font:inherit; }
#inquire-actions { display:flex; gap:.5rem; }
#inquire-status:empty { display:none; }
#inquire-status { margin:0; font-size:.8rem; }

/* Big drag readout. Fixed and pointer-events:none, so it can never shift the
   page or cover a control it would block. */
:root { --drag-fade:180ms; }
/* Narrow: across the top, clear of the slider and buttons lower down.
   Wide: out at the side, vertically centred, clear of the column. */
#drag-readout { position:fixed; left:0; right:0; top:0; text-align:center;
  background:var(--bg); padding:.25rem 0 .35rem;
  font-weight:800; font-size:clamp(2.5rem,13vw,4.5rem); line-height:1;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
  color:#000; pointer-events:none; opacity:0; visibility:hidden;
  transition:opacity var(--drag-fade) ease-out, visibility 0s linear var(--drag-fade);
  z-index:10; }
#drag-readout.on { opacity:1; visibility:visible; transition:opacity 0s, visibility 0s; }
/* The side readout is the one thing on the page that is NOT in the column and
   still has to keep out of it. It was sized as a fraction of the viewport
   (9vw), which silently assumed a short string: this was drawn for $20.00, and
   the slider's ceiling is $5,000.00 — 4.16em against 2.86em in this face at 800
   condensed, half again as wide. At 9vw that ran 66px into the column at
   1280px, over the slider and the size bars.

   So the type is sized from the space it has to fit in rather than from the
   viewport. The column is centred, so the gutter beside it is
   (100vw - var(--col-box)) / 2; take off the 3vw the readout is held off the
   screen edge and 1.5rem of air, and divide by the width of the longest string
   the slider can ever show. Below 64rem there is no gutter worth the name — the
   division lands under 4rem — so the top band keeps the job, and being
   full-width it cannot overlap the column sideways at all.

   4.16 is $5,000.00 measured, not chosen. Remeasure it if SLIDER_MAX_CENTS
   gains a digit or the face changes; test/preview-drift.test.ts fails if the
   first happens without this being looked at. */
@media (min-width: 64rem) {
  #drag-readout { left:auto; right:3vw; top:50%; transform:translateY(-50%);
    text-align:right; background:none; padding:0;
    font-size:min(9rem,
      calc(((100vw - var(--col-box)) / 2 - 3vw - 1.5rem) / 4.16)); }
}
@media (prefers-reduced-motion: reduce) {
  #drag-readout { transition:none; }
}
/* app.js mirrors the same query onto <html> so the state object and the
   stylesheet can never disagree about it. */
.reduced-motion #drag-readout { transition:none; }
input, select, button { font:inherit; padding:.6rem; border:1px solid #000;
  background:#fff; color:#000; }
button { text-transform:lowercase; cursor:pointer; }
button:disabled { opacity:.4; cursor:not-allowed; }
/* One focus ring for the whole page, in the page's own ink rather than the
   browser default. */
input:focus-visible, button:focus-visible, a:focus-visible, summary:focus-visible {
  outline:2px solid #000; outline-offset:2px; }
#size-bars { display:flex; width:100%; }
.size-bar { flex:1; padding:.6rem 0; border:1px solid #000; background:#fff; color:#000;
  text-transform:lowercase; cursor:pointer; margin-left:-1px; }
.size-bar:first-child { margin-left:0; }
.size-bar[aria-checked="true"] { background:#000; color:#fff; }
/* Visually hidden but still in the accessibility tree, so aria-describedby and
   the live region resolve. Never display:none, which would remove them. */
.sr-only { position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap;
  border:0; }

/* --- buy button, and the one loading affordance on the page ---------------
   The garment image deliberately has none: it holds the last good photograph.
   This is the only place the page admits to waiting. */
#submit-row { position:relative; display:grid; }
#submit { display:flex; align-items:center; justify-content:center; gap:.5rem; }
#submit[aria-disabled="true"] { opacity:.55; cursor:progress; }
#submit-spinner { display:none; }
/* A plain monochrome ring with one open quadrant. No icon font, no SVG. */
#submit-row.updating #submit-spinner { display:block; width:.8em; height:.8em;
  border:2px solid currentColor; border-top-color:transparent; border-radius:50%;
  animation:submit-spin .8s linear infinite; }
@keyframes submit-spin { to { transform:rotate(360deg); } }
/* Reduced motion: keep the indicator, drop the rotation. */
@media (prefers-reduced-motion: reduce) {
  #submit-row.updating #submit-spinner { animation:none; border-top-color:currentColor;
    opacity:.45; }
}
.reduced-motion #submit-row.updating #submit-spinner { animation:none;
  border-top-color:currentColor; opacity:.45; }

/* Tooltip, on pointer hover AND keyboard focus, never hover-only. */
/* Below the button, not above: above would sit on top of the size bars. */
#submit-tip { position:absolute; top:calc(100% + .4rem); left:50%;
  transform:translateX(-50%); z-index:5; pointer-events:none;
  background:#000; color:#fff; font-size:.7rem; line-height:1;
  padding:.35rem .5rem; white-space:nowrap; opacity:0; visibility:hidden; }
#submit-row.updating #submit:hover ~ #submit-tip,
#submit-row.updating #submit:focus-visible ~ #submit-tip { opacity:1; visibility:visible; }

/* --- disclosure ----------------------------------------------------------
   Sits below the buy action and opens downward, so nothing above it moves. */
#detail { border-top:1px solid #000; margin-top:1.5rem; padding-top:.6rem; }
#detail > summary { cursor:pointer; font-size:.75rem; text-transform:lowercase;
  list-style:none; display:flex; align-items:center; gap:.4rem; padding:.15rem 0; }
#detail > summary::-webkit-details-marker { display:none; }
/* Explicit open/closed affordance drawn in CSS: a chevron that rotates. */
#detail > summary::before { content:""; width:.4rem; height:.4rem; flex:none;
  border-right:1.5px solid currentColor; border-bottom:1.5px solid currentColor;
  transform:rotate(-45deg); transform-origin:center; }
#detail[open] > summary::before { transform:rotate(45deg); }
#detail > summary:focus-visible { outline:2px solid #000; outline-offset:3px; }
#fit { font-size:.75rem; min-height:1.2em; margin:.5rem 0 .6rem; }
#breakdown { margin-top:0; }
/* The figures count from the old value to the new one (see renderBreakdown in
   app.js). While a newer quote is in flight the numbers on screen are real but
   not current, so they are dimmed rather than removed: emptying and refilling
   the table on every change is the flicker this replaces. */
#breakdown td { font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1; }
#breakdown.stale { opacity:.45; }
#breakdown { transition:opacity 120ms ease-out; }
@media (prefers-reduced-motion: reduce) { #breakdown { transition:none; } }
.reduced-motion #breakdown { transition:none; }
/* Footer. A stacked list, aligned to the same optical column as the page body
   (main's 40rem measure), set well below the buy action so it reads as a
   separate region rather than more content. No rules, fills, bullets or icons:
   the separation is done with space and with a step down in weight. */
/* Same box as main.col, so the list's left and right edges land exactly on the
   size bars' at every width, including narrow, where both sit against the
   1rem gutter. */
footer { max-width:var(--col-box); margin:0 auto; padding:5rem var(--gutter) 4rem; }
footer ul { list-style:none; margin:0; padding:0; }
footer li { margin:0; }
footer a { display:inline-block; padding:.3rem 0; font-size:.75rem;
  line-height:1.35; letter-spacing:.01em; color:#555; text-decoration:none; }
footer a:hover { color:#000; text-decoration:underline; text-underline-offset:.2em; }
footer a:focus-visible { outline:2px solid #000; outline-offset:3px; color:#000; }
@media (min-width: 40rem) {
  footer { padding-top:6rem; }
}
/* --- the thank-you page (public/success.html) ----------------------------
   Two zones, and the difference between them is the page's whole structure.

   The top zone is the receipt: photograph, the printed line, the headline, the
   facts, the id. It is CENTRED, because every item in it is a short, singular
   fact about one object, and because centring is what makes the id read as the
   thing you came back for rather than the end of a sentence.

   The bottom zone is instructions: the timeline and the help line. It is left
   aligned, because prose and ordered lists are read, not looked at.

   The two hairlines are the only rules on the page and each one starts a zone.
   Nothing here introduces a colour, a shadow or a second accent; the page is
   the same ink on the same paper as the shop, set at different sizes. */

/* The frame while the order is still being looked up. This page cannot ship a
   default photograph in its markup the way the product page does — the shirt it
   shows is the one that was bought, and until the lookup returns that is not
   known. So it holds a skeleton rather than an empty square, which on a page
   reached straight from a payment reads as a purchase that failed. */
#preview-frame.loading { background:#f2f2f2; animation:skeleton-pulse 1.6s ease-in-out infinite; }
@keyframes skeleton-pulse { 50% { opacity:.45; } }
@media (prefers-reduced-motion: reduce) {
  #preview-frame.loading { animation:none; }
}
.reduced-motion #preview-frame.loading { animation:none; }

.success #statement { text-align:center; }
/* The one genuinely large thing on the page. The shop's type tops out at the
   statement (1.5rem) and the drag readout (a transient overlay), so this is
   where the page gets its focal point: a clear step up in scale and weight from
   everything under it, in the same condensed lowercase as the header. */
.success #thanks { text-align:center; margin:1.25rem 0 0;
  font-size:clamp(2.25rem,11vw,3.25rem); font-weight:800; line-height:.95;
  letter-spacing:-.03em; text-transform:lowercase; }
/* The headlines that are sentences rather than the two-word thank-you. Same
   weight and case, one clear step down in size, because a sentence set to fill
   the column is shouting — and the things it says here ("this checkout was not
   completed") are the last things on the site worth shouting. */
.success #thanks.plain { font-size:clamp(1.5rem,6.5vw,2rem); line-height:1.1;
  letter-spacing:-.02em; }
/* No photograph above, so the headline takes over the frame's opening space. */
.success.no-garment #thanks { margin-top:3rem; }
.success #facts { text-align:center; margin:.4rem 0 0; font-size:.85rem; color:#555;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1; }

/* Anything true of this order the timeline does not cover: an order being
   checked by hand, a submission that failed. Boxed, because it is the one thing
   on the page that may need reading twice, and left aligned inside the centred
   zone, because it is a sentence rather than a fact. */
.success #note { margin:1.25rem 0 0; padding:.7rem .8rem; border:1px solid #000;
  font-size:.8rem; line-height:1.45; text-align:left; }

/* The order id. Mono is the only typeface change on the site and it is doing a
   job: this is a 66-character machine string that people read aloud, retype and
   paste into an email, and the shop's condensed sans makes l/1/I and 0/O
   ambiguous at the size it has to be set. The tinted field and the whole-string
   selection are the affordance — the button is the shortcut, not the only way. */
.success #order-id-block { margin:2.25rem 0 0; padding-top:1.1rem;
  border-top:1px solid #000; text-align:center; }
.success #order-id-label { margin:0; font-size:.7rem; letter-spacing:.09em;
  text-transform:lowercase; color:#555; }
/* The reference (see src/order-number.ts) is the PRIMARY thing shown here:
   short enough to read aloud and to quote in an email subject line. It carries
   the IPFTS- prefix and hex letters specifically so it CANNOT be read as the
   all-digit number on Stripe's receipt — a buyer holds both at once, and the
   two used to be identically shaped. Set larger and bolder than the session id
   below it. */
.success #order-number { margin:.5rem 0 .7rem; font-size:1.35rem;
  font-weight:700; letter-spacing:.04em;
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
/* Names the other number the buyer is holding. Without it the page reads as
   though it is contradicting the receipt in their inbox. */
.success #stripe-note { margin:.9rem 0 0; font-size:.7rem; line-height:1.45;
  color:#555; }
.nowrap { white-space:nowrap; }
.success #copy-order { font-size:.75rem; padding:.4rem .9rem; }


/* The instructions zone. */
.success #next { margin:2.5rem 0 0; padding:1.1rem 0 0 1.1rem;
  border-top:1px solid #000; font-size:.85rem; line-height:1.45; }
.success #next li { margin:0 0 .55rem; padding-left:.25rem; }
.success #next li:last-child { margin-bottom:0; }
.success #help { margin:1.25rem 0 0; font-size:.75rem; line-height:1.45; color:#555; }
.success #help a { color:#000; }
.success #home { margin:1.5rem 0 0; text-align:center; font-size:.85rem; }

.doc { max-width:34rem; margin:0 auto; padding:2rem 1rem 4rem; font-size:.9rem; }
.doc h1 { font-size:1rem; text-transform:lowercase; }
table.plain { border-collapse:collapse; width:100%; font-size:.85rem; }
table.plain td, table.plain th { border:1px solid #000; padding:.4rem .5rem; text-align:left; }
/* The line you draw under a column of figures before writing what is left:
   everything above it is a cost, everything below it is not. Width only —
   the cell already has a 1px border, and under border-collapse the wider of
   two shared edges wins, so this replaces the gridline between the last cost
   and profit rather than stacking a second line on top of it. */
table.plain tr.subtotal > * { border-top-width:3px; }

/* The slider and the product photo must not start a native drag.
   `draggable="false"` on the image covers the image itself; these rules cover
   the rest of the gesture — a press that begins on the track used to select
   text and hand the cursor a drag ghost (the page/globe icon) on most drags.
   app.js also calls preventDefault() on pointerdown; this is the CSS half, so
   the behaviour does not depend on a listener running first. */
#price-track, #price-track * { user-select:none; -webkit-user-select:none; }
#mockup { -webkit-user-drag:none; user-drag:none; }
