/* ═══════════════════════════════════════════════════════════════════════════
   GACETA OFFICIAL — single-page storefront.

   The brand name is GACETA OFFICIAL, two Fs, matching the domain
   gacetaofficial.com. "Oficial" with one F is the Spanish word and is correct
   in that language. See docs/wikipedia.txt for the brand background; this file
   ships, so it carries the identification and not the history.

   The whole thing is one document: the 3D stage, the two tabs and the centred
   windows are all layers over the same framebuffer. Panels are solid paper laid
   over a live scene, not a second site, so the room never stops existing
   behind them.

   Hard edges only. No radius, no shadow, no gradient. Type is condensed,
   uppercase and widely tracked; the only decoration is a 1px rule.
   ═══════════════════════════════════════════════════════════════════════════ */

@font-face {
  font-family: 'Gaceta Condensed';
  src: url('fonts/gloucester-mt-extra-condensed.ttf') format('truetype');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'Gaceta Display';
  src: url('fonts/Trigram-Regular.ttf') format('truetype');
  font-weight: 400;
  font-display: swap;
}

:root {
  --ink: #0b0b0b;
  --paper: #ffffff;
  /* The muted grey. It was #767676, which is a colour tuned for white paper:
     at 4.5:1 against white it reads as secondary and as nothing else. On the
     Klein blue it is 1.5:1 against the field, which is not "muted", it is
     invisible - the ARRASTRA PARA GIRAR cue and the pre-sales line both went
     effectively blank when the panels stopped being white.

     This is the same job in the new field: light enough to read as white's
     quieter sibling, not so light that it competes with it. 6:1 against
     #002FA7, which is what the labels want. */
  --grey: #bcc6f0;
  --hair: 1px solid var(--ink);

  --tab: clamp(268px, 30vw, 452px);
  --mark: clamp(30px, 4.2vh, 44px);
  --edge: 6px;                       /* how far the hover border sits outside the mark */
  --head: clamp(54px, 8.5vh, 82px); /* clearance under the header marks */
  --ease: cubic-bezier(0.16, 1, 0.3, 1);

  /* Display: the condensed caps that carry every big title. Titles are set at
     the face's real weight — synthetic bold and a stroke were both tried here
     and at display sizes the stroke read as a rendering fault. */
  --f-dis: 'Gaceta Condensed', 'Gaceta Display', 'Haettenschweiler', 'Arial Narrow', sans-serif;
  /* Labels, nav, buttons: the same face, tracked out. */
  --f-ui: 'Gaceta Condensed', 'Haettenschweiler', 'Arial Narrow', system-ui, sans-serif;
  /* Anything a person has to read or fill in. The condensed face is a typeface,
     not a text face — at 15px it goes thin and crowded. */
  --f-body: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}

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

/* The page under the page is the flag blue, not white.

   `html` carries the colour and `body` paints over it in white, which is what
   makes this work: the root element is what a browser shows when there is
   nothing to show. On Safari that is the overscroll rubber-band at the top and
   bottom of the page, and the edges during the elastic bounce after a drag. In
   white, a dark thermal stage is framed by white gutters that read as a broken
   screenshot; in #01206B it reads as the stage continuing off the edge.

   #01206B is the blue of the Venezuelan flag. Not decoration: it is the same
   colour the backdrop is built from, so the frame and the picture agree. */
html {
  height: 100%;
  margin: 0;
  background: #01206B;
}

body {
  height: 100%;
  margin: 0;
  background: #002FA7;
}

body {
  overflow: hidden;
  overscroll-behavior: none;
  /* White, not ink. The field under every panel and every window is Klein blue
     now, and #0b0b0b on it is a colour with nothing to say. One declaration
     here is most of the fix: titles, catalogue copy, the X buttons and the
     buttons all inherit, and the ones that set a colour of their own are the
     handful of rules below that say `color: var(--ink)` and are being walked
     through one at a time. */
  color: var(--paper);
  font-family: var(--f-ui);
  font-size: 13px;
  line-height: 1.35;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

button, input, textarea, select { font: inherit; letter-spacing: inherit; color: inherit; }
button { background: none; border: 0; padding: 0; margin: 0; cursor: pointer; }
img { display: block; }

::selection { background: var(--ink); color: var(--paper); }

/* ── 3D stage ───────────────────────────────────────────────────────────── */

#container { position: fixed; inset: 0; z-index: 1; overflow: hidden; }
#container canvas { display: block; }

/* The loading mark. Three of them, pulsing in sequence.

   It used to be a black chip reading ISSUING 42%, which sat over the live stage
   as a rectangle of flat UI on top of an image that is doing a great deal of
   work. Three marks of the gazette pulsing like an ellipsis belongs to the same
   world as the backdrop instead, and it costs the same to draw.

   Each mark is offset by --i, so the animation is one rule with three delays
   rather than three near-identical rules. The mark is the same logo the about
   button uses, at 34px, which is small enough to read as punctuation and large
   enough to be recognised. */
#loading {
  position: fixed;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  z-index: 4000;
  pointer-events: none;
}

/* Eight stars, on the arc they sit on in the flag.

   Laid out by trigonometry rather than by hand: each star is rotated by its
   index onto a circle and pushed out from the centre, so the arc is a real arc
   and not eight positions somebody eyeballed. `--n` is written in the markup,
   once, and every other number in the rule is derived from it.

   The radius and the per-star angle are chosen so the eight fit inside a
   150px box on a 390px phone, which is the smallest screen this has to work on.
   `sin`/`cos` cannot be used in CSS, so the offsets are the eight values
   themselves - there are only eight. */
.stars {
  display: block;
  position: relative;
  width: 150px;
  height: 76px;
}

.stars i {
  --angle: calc(var(--n) * 22.5deg - 78.75deg);
  position: absolute;
  left: 50%;
  top: 50%;
  width: 11px;
  height: 11px;
  margin: -5.5px 0 0 -5.5px;
  /* Rotate about the centre of the box so the arc opens downward, the way the
     flag's does, then rotate each star about itself so they all point up. */
  transform:
    rotate(var(--angle))
    translateY(-62px)
    rotate(calc(var(--angle) * -1));
  /* The five-point star. Two overlapping squares' worth of points, which is the
     cheapest way to get a recognisable star without an asset. */
  clip-path: polygon(
    50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%,
    50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%
  );
  /* White. The backdrop these load over is Klein blue, and a blue star on a
     blue field is the same nothing that a blue letter would be - which is
     exactly what they were for the one release after the field changed colour.
     The flag's stars are gold; the flag's stars here are the white every other
     mark on the page inverts to. */
  background: var(--paper);
  /* Unlit stars sit DIM, not transparent. A transparent star reads as a hole
     in the picture rather than as a star that has not come on yet. */
  opacity: 0.22;
  transition: opacity 0.3s linear, transform 0.3s var(--ease);
}

/* A star is lit when the loader says how many are. main.js writes a count onto
   `data-lit` as the bytes arrive, so the fill is driven by real progress and not
   by a timer. One selector per possible count, because there are only eight and
   CSS cannot do a dynamic nth-child().

   Deliberately a data attribute and not a custom property matched with
   [style*=...]: that is a substring test against a serialised string, and the
   serialisation of a custom property is not something a stylesheet should be
   depending on. */
#loading[data-lit='1'] i:nth-child(-n+1),
#loading[data-lit='2'] i:nth-child(-n+2),
#loading[data-lit='3'] i:nth-child(-n+3),
#loading[data-lit='4'] i:nth-child(-n+4),
#loading[data-lit='5'] i:nth-child(-n+5),
#loading[data-lit='6'] i:nth-child(-n+6),
#loading[data-lit='7'] i:nth-child(-n+7),
#loading[data-lit='8'] i:nth-child(-n+8) { opacity: 1; }

/* ── Containing the loader ───────────────────────────────────────────────
   Eight elements each running their own clip-path and transform is enough
   compositor work on its own; the box is small and it promotes once, so the
   whole thing is treated as a single layer while it is on screen. */
#loading .stars { will-change: opacity; }

/* `hidden` has to be re-asserted here.

   The UA stylesheet implements [hidden] as `display: none`, but this rule sets
   `display: flex` on #loading itself, and an author rule always beats the UA
   one regardless of specificity. So the loader ignored its own hidden
   attribute and stayed on screen for the whole session, parked over whatever
   was in the middle of the viewport. main.js hides it by setting .hidden = true,
   exactly as it always did; nothing was wrong on the JS side and the loader
   simply refused to go.

   Same line exists for #toast and for .lang__menu, which have the same shape. */
#loading[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  /* Still reads as a loader: the arc still fills, it just does not slide. */
  .stars i { transition: none; }
}

/* ── Stage furniture: the clickable garments' rings and tags ────────────────
   One .garment-mark per garment, cloned out of #garment-mark in the markup. The
   wrapper exists only to hold the pair together for removal; both children are
   position:fixed and are placed independently. */

#stage-ui {
  position: fixed;
  inset: 0;
  z-index: 900;
  pointer-events: none;
  mix-blend-mode: difference;   /* the ring inverts over whatever is behind it */
  color: var(--paper);
}

.garment-ring {
  position: fixed;
  top: 0; left: 0;
  width: 0; height: 0;
  /* 2px, not 1.5px.

     A 1.5px border is snapped DOWN to 1px by Chrome at devicePixelRatio 1 — it
     rounds border widths to whole device pixels — so the thicker line that was
     asked for never actually rendered on a standard display. 2px is the
     smallest value that is unambiguously heavier than the 1px it replaced, and it
     holds at any ratio. At 1px the square all but vanished against the thermal
     backdrop, which is high-frequency and won every comparison at exactly the
     moments the ring needed to read: when the piece is small on screen and you
     are deciding whether to click it. */
  border: 2px solid currentColor;
  opacity: 0;
  transition: opacity 0.18s linear;
  will-change: opacity, transform;
}

/* The name and its description, under the ring. Both are ALWAYS on — see
   tickGarments(), which drives the opacity of the whole pair off whether the
   piece is on screen, never off whether it is hovered. The hovered piece simply
   goes to full strength, so it reads as the loud one without the others
   disappearing. */
.garment-tag {
  position: fixed;
  top: 0; left: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 9px;
  letter-spacing: 0.22em;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.18s linear;
  will-change: opacity, transform;
}

/* The description. Sits under the name, quieter, and is the print's blurb rather
   than a UI instruction — the cue line is only shown on hover, because telling
   an unhovered visitor what to do with it is noise. */
.garment-tag .garment-tag__note {
  color: currentColor;
  opacity: 0.62;
  font-size: 8px;
  letter-spacing: 0.18em;
}

.garment-tag .garment-tag__cue { color: currentColor; opacity: 0.62; }

/* ── Header marks ───────────────────────────────────────────────────────────
   White children composited inside the button, then the button itself is
   differenced against the page. One rule covers every case: the mark reads as
   a photographic negative over the thermal backdrop, over a white tab and over
   a white window, and never has to change colour. */

.mark {
  position: fixed;
  z-index: 3000;
  mix-blend-mode: difference;
  background: none;
  border: 0;
  padding: 0;
  color: var(--paper);
  cursor: pointer;
  outline: none;
}

.mark__ink { position: absolute; inset: 0; background: currentColor; -webkit-mask-size: 100% 100%; mask-size: 100% 100%; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; -webkit-mask-position: center; mask-position: center; }
.mark__img { width: 100%; height: 100%; }

/* The interaction border: flat, square, hard. It blinks twice on approach and
   then holds — a single strobe on entry reads as a glitch, two read as a
   signal. Once a panel is open it stays lit, because the panel is the state. */
.mark__edge {
  position: absolute;
  inset: calc(var(--edge) * -1);
  border: 1px solid currentColor;
  opacity: 0;
  transition: opacity 0.14s linear;
}

.mark__fill {
  position: absolute;
  inset: calc(var(--edge) * -1);
  background: currentColor;
  opacity: 0;
  transition: opacity 0.3s var(--ease);
}

@keyframes edge-flash {
  0%   { opacity: 1; }
  16%  { opacity: 0; }
  34%  { opacity: 1; }
  52%  { opacity: 0; }
  70%, 100% { opacity: 1; }
}

@media (hover: hover) {
  .mark:hover .mark__edge { animation: edge-flash 0.44s steps(1, end) 1 forwards; }
}

/* Touch has no hover to blink on, so this used to rest the border lit at 45%
   permanently, on the reasoning that the affordance had to survive being on a
   phone.

   It does not work. Four permanently boxed squares across the top of the screen
   and one in the corner read as form fields, or as a selection, or as something
   that is currently hovered. Every one of them is always doing it, so the signal
   carries no information and the marks just look outlined in a mistake.

   A mark on a phone does not need a border to be findable: it is a large
   high-contrast square sitting in a corner. What it needs is feedback on PRESS,
   which is the touch equivalent of the hover blink and which `.mark:active`
   below already provides. */
@media (hover: none) {
  .mark .mark__edge { opacity: 0; }
}

.mark:focus-visible .mark__edge,
.mark[aria-expanded='true'] .mark__edge { opacity: 1; }
.mark:focus-visible { outline: none; }

.mark:active .mark__fill { opacity: 1; transition: none; }
.mark:active .mark__ink { opacity: 0.25; transition: opacity 0.4s var(--ease); }

.mark--tl { left: 18px; top: 14px; width: var(--mark); aspect-ratio: 472 / 512; }
.mark--tr { right: 18px; top: 14px; width: var(--mark); aspect-ratio: 472 / 512; }
.mark--tc { left: 50%; top: 6px; transform: translateX(-50%); width: clamp(120px, 27vh, 380px); aspect-ratio: 1170 / 170; }
.mark--bl { left: 16px; bottom: 16px; width: clamp(46px, 9vh, 58px); aspect-ratio: 1; }

.mark__ink--catalog { -webkit-mask-image: url(images/catalog.webp); mask-image: url(images/catalog.webp); }
.mark__ink--cart { -webkit-mask-image: url(images/cart.webp); mask-image: url(images/cart.webp); }
.mark__ink--text { -webkit-mask-image: url(images/textlogo.webp); mask-image: url(images/textlogo.webp); }

/* Cart tally, hung off the bag. Plain ink and deliberately outside the mark's
   blend group — see the note in the HTML. */
/* Language. The SAME inversion as the header marks, at letter size: a white
   glyph composited inside the button, then the button itself differenced
   against the page - so the letter reads as a negative over the Klein blue
   field, over a panel and over a window, and never has to switch colour.

   It used to carry a flat plate of its own with a dark letter on it, which
   made it the one mark on the page that did not invert - and when the field
   behind everything became the same blue the plate was made of, that plate
   differenced to NOTHING and took the letter with it: a black square with a
   near-invisible glyph on it. A solid colour blended against the same solid
   colour is zero, which is why every other mark here is a white SHAPE on a
   transparent ground.

   The letter is the whole mark, so unlike the icons it cannot be a mask - it
   is `color`, and it is white. The states then follow the grammar the icons
   use: the edge blinks twice on approach and stays lit while the menu is open,
   and the press fills the plate and knocks the letter out. The fill is the
   button's own background because a text node cannot carry a child, and the
   edge is an outline because a border that thick would move the letter. */
.mark--lang {
  /* Stacked UNDER the catalogue mark on a phone - see the desktop layout,
     which puts it back beside that mark where there is room for the pair. */
  left: calc(18px + (var(--mark) - 28px) / 2);
  right: auto;
  top: calc(14px + var(--mark) + 10px);
  width: 28px;
  height: 28px;
  background: none;
  color: var(--paper);
  display: grid;
  place-items: center;
  font-family: var(--f-ui);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.06em;
  line-height: 1;
  /* Transparent at rest; the hover blink and the open menu paint it. */
  outline: 1px solid transparent;
  outline-offset: var(--edge);
  transition: transform 0.34s var(--ease), background 0.14s linear, color 0.14s linear;
}

/* The press. This is `.mark:active .mark__fill` for a mark whose glyph is a
   text node: the plate comes up behind the letter and the letter goes with it. */
.mark--lang:active { background: var(--paper); color: var(--ink); }

@media (hover: hover) {
  .mark--lang:hover {
    outline-color: currentColor;
    animation: lang-edge 0.44s steps(1, end) 1 forwards;
  }
}

/* The same steps as `edge-flash` above, on the one property this mark can
   animate - it has no child to fade, so the outline's colour is the edge. */
@keyframes lang-edge {
  0%   { outline-color: currentColor; }
  16%  { outline-color: transparent; }
  34%  { outline-color: currentColor; }
  52%  { outline-color: transparent; }
  70%, 100% { outline-color: currentColor; }
}

.mark--lang:focus-visible,
.mark--lang[aria-expanded='true'] { outline-color: currentColor; }

.lang__menu {
  position: fixed;
  top: calc(14px + var(--mark) + 42px);
  left: calc(18px + (var(--mark) - 28px) / 2);
  right: auto;
  z-index: 3002;
  min-width: 44px;
  background: #002FA7;
  color: var(--paper);
  border: var(--hair);
  transition: transform 0.34s var(--ease);
}
.lang__menu[hidden] { display: none; }
.lang__opt {
  display: block;
  width: 100%;
  padding: 8px 12px;
  background: #002FA7;
  color: var(--paper);
  font-family: var(--f-ui);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-align: center;
}
.lang__opt + .lang__opt { border-top: var(--hair); }
.lang__opt:hover { background: var(--ink); color: var(--paper); }
.lang__opt[aria-checked='true'] { background: var(--ink); color: var(--paper); }
.lang__opt:focus-visible { outline: 1px solid var(--ink); outline-offset: -2px; }

/* ── The language mark on a desktop ────────────────────────────────────────
   The exact complement of the sheet query at the bottom of the file: a phone
   or an upright tablet gets the bottom sheet, and there the mark stays stacked
   under the catalogue and never moves. Everything on a side-panel screen gets
   both of the things below.

   1. The mark moves UP BESIDE the catalogue mark - same row, centred on it -
      because on a screen with room for two controls in one corner, one under
      the other is a column nobody asked for.

   2. It walks RIGHT by exactly `--tab` when the catalogue opens, and the menu
      walks with it, so the pair ends up clear of the panel's edge rather than
      sitting on top of it. `--tab` IS the panel's width, so the open position
      is stated rather than measured, and store.js writes `data-panel='left'`
      on the root element for exactly this. The right panel is not handled
      here: it is on the other side of the screen and covers nothing in this
      corner, which is why the old `right-open` dodge is gone. */
@media (min-width: 641px) and (orientation: landscape),
       (min-width: 901px) and (orientation: portrait) {
  .mark--lang {
    left: calc(18px + var(--mark) + 10px);
    top: calc(14px + (var(--mark) * 512 / 472 - 28px) / 2);
  }
  .lang__menu {
    left: calc(18px + var(--mark) + 10px);
    top: calc(14px + (var(--mark) * 512 / 472 - 28px) / 2 + 34px);
  }
  html[data-panel='left'] .mark--lang,
  html[data-panel='left'] .lang__menu { transform: translateX(var(--tab)); }
}

/* Cart tally and the garment scan readout are both plain ink plates, so they
   live outside the marks' blend groups — see the notes in the HTML. */
.tally,
#garment-scan {
  position: fixed;
  z-index: 3001;
  background: var(--ink);
  color: var(--paper);
  pointer-events: none;
}

.tally {
  top: 10px;
  right: calc(18px + var(--mark) + 3px);
  min-width: 15px;
  padding: 1px 3px;
  font-size: 9px;
  line-height: 1.2;
  letter-spacing: 0.1em;
  text-align: center;
}
.tally[hidden] { display: none; }

/* Scanner readout, hung off the garment's top-left corner while the pointer is
   on it. Same plate as the ISSUING loader, because it is the same idea. */
#garment-scan {
  top: 0; left: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 5px 8px;
  font-size: 9px;
  line-height: 1.3;
  letter-spacing: 0.2em;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.14s linear;
}
#garment-scan .scan__state { color: #9a9a9a; }

/* Bottom-right colophon. Differenced against the page like the header marks, so
   it holds its value over the thermal field and over white paper alike. */
.colophon {
  position: fixed;
  right: 18px; bottom: 16px;
  z-index: 800;
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 5px;
  font-size: 8px;
  line-height: 1;
  letter-spacing: 0.26em;
  text-align: right;
  color: var(--paper);
  mix-blend-mode: difference;
  user-select: none;
}

.colophon__row { display: flex; align-items: baseline; gap: 7px; white-space: nowrap; }
.colophon__row + .colophon__row { opacity: 0.72; }

/* A hairline, not a bullet. It has to disappear into the field at this size. */
.colophon__sep { opacity: 0.5; }

.colophon a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color 0.14s linear, opacity 0.14s linear;
}
.colophon a:hover,
.colophon a:focus-visible { border-bottom-color: currentColor; opacity: 1; }

/* ── Tabs ────────────────────────────────────────────────────────────────── */

.tab {
  position: fixed;
  top: 0;
  height: 100%;
  width: var(--tab);
  z-index: 1200;
  display: flex;
  flex-direction: column;
  background: #002FA7;
  color: var(--paper);
  visibility: hidden;
  transition: transform 0.34s var(--ease), visibility 0s linear 0.34s;
}

.tab--left {
  left: 0;
  border-right: var(--hair);
  transform: translateX(-100%);
}
.tab--right {
  right: 0;
  border-left: var(--hair);
  transform: translateX(100%);
}
.tab.is-open {
  visibility: visible;
  transform: translateX(0);
  transition: transform 0.34s var(--ease), visibility 0s;
}

.tab__head {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: var(--head) 18px 12px;
  border-bottom: var(--hair);
}

/* The grip. Inert on the desktop side panels, where there is no height to
   change, so it is hidden rather than merely unstyled. */
.tab__grip {
  display: none;
  flex: 0 0 auto;
  position: relative;
  height: 22px;
  padding: 0;
  background: none;
  border: 0;
  cursor: grab;
  touch-action: none;
}
.tab__grip::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 42px;
  height: 3px;
  background: var(--paper);
  opacity: 0.26;
}
.tab__grip:focus-visible { outline: 2px solid var(--paper); outline-offset: -2px; }
.tab.is-dragging .tab__grip { cursor: grabbing; }
.tab.is-dragging .tab__grip::after { opacity: 0.6; }

.tab__title {
  margin: 0;
  font-family: var(--f-ui);
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 0.26em;
}

.tab__meta {
  margin-left: auto;
  font-size: 10px;
  letter-spacing: 0.18em;
  color: var(--grey);
}

.tab__close {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  margin-left: auto;   /* nothing else may sit between the title and the close */
  font-size: 17px;
  line-height: 1;
  letter-spacing: 0;
  transition: transform 0.2s var(--ease);
}
.tab__close:hover { transform: rotate(90deg); }

.tab__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 14px 18px 20px;
  scrollbar-width: thin;
  scrollbar-color: var(--paper) transparent;
}
.tab__body::-webkit-scrollbar { width: 6px; }
.tab__body::-webkit-scrollbar-thumb { background: var(--paper); }

/* The logo mark floats over the paper, so the catalogue leaves it room. */
.tab--left .tab__body { padding-bottom: calc(clamp(46px, 9vh, 58px) + 30px); }

/* ── Catalogue grid: three across, square, hard-edged, no names ───────────── */

.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }

.thumb {
  position: relative;
  display: block;
  aspect-ratio: 1;
  border: var(--hair);
  background: #002FA7;
  overflow: hidden;
}
.thumb img { width: 100%; height: 100%; object-fit: cover; }
.thumb__plate { width: 100%; height: 100%; }
.thumb:hover img,
.thumb:focus-visible img { filter: invert(1); }
.thumb:focus-visible { outline: 1px solid var(--ink); outline-offset: 2px; }
/* The piece you are looking at is shown inverted inside a heavier frame. A
   hatch pattern was the first attempt and it read as an image still loading. */
.thumb[aria-current='true'] { box-shadow: inset 0 0 0 2px var(--ink); }
.thumb[aria-current='true'] img { filter: invert(1); }

/* ── Colourway swatches: under the name, on the product's own page ───────────
   The grid shows one tile per product, always, and a product with colourways
   shows its default. These are the alternatives, in the panel, where there is
   room to name them and where the visitor is already looking at that garment.

   A swatch is a small photograph of the colourway, not a colour chip: the whole
   problem with a recoloured print is that you cannot tell it from the original
   without seeing the artwork on it. */

.swatches { margin: 14px 0 0; }
.swatches__label {
  margin: 0 0 7px;
  font-size: 9px;
  letter-spacing: 0.24em;
  color: var(--grey);
}
.swatches__row { display: flex; flex-wrap: wrap; gap: 6px; }
.swatch {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 4px 9px 4px 4px;
  border: var(--hair);
  background: #002FA7;
  color: var(--paper);
  transition: background 0.14s linear, color 0.14s linear;
}
.swatch img {
  width: 30px;
  height: 30px;
  object-fit: cover;
  border: 1px solid var(--hair);
}
.swatch__label { font-size: 9.5px; letter-spacing: 0.16em; }
.swatch:hover { background: var(--ink); color: var(--paper); }
.swatch:hover img { border-color: var(--paper); }
.swatch:focus-visible { outline: 1px solid var(--ink); outline-offset: 2px; }
.swatch[aria-pressed='true'] { background: var(--ink); color: var(--paper); }
.swatch[aria-pressed='true'] img { border-color: var(--paper); }

/* ── Right tab: product view ────────────────────────────────────────────── */

.plate {
  border: var(--hair);
  background: #002FA7;
  aspect-ratio: 1;
  overflow: hidden;
}
.plate img { width: 100%; height: 100%; object-fit: cover; }

/* ── The catalogue 3D viewer ──────────────────────────────────────────────
   A turntable in the right panel: square, hard-edged like every other plate,
   no radius and no shadow. The photograph sits UNDER the canvas rather than
   beside it, so there is never a blank while the mesh and the HDRI are on the
   wire — and the canvas is opaque the moment it has something to show, so the
   two are never both visible. `is-failed` puts the photograph back, which is
   the honest thing to do when the mesh could not be loaded at all. */

.viewer__block { margin: 0 0 2px; position: relative; }

.viewer__wrap {
  position: relative;
  aspect-ratio: 1;
  border: var(--hair);
  background: #002FA7;
  overflow: hidden;
}
.viewer__wrap > .plate--under {
  position: absolute;
  inset: 0;
  border: 0;
  /* Hidden under an opaque canvas; kept for the load and the failure. */
  opacity: 1;
  transition: opacity 0.24s var(--ease);
}
.viewer__wrap.is-live > .plate--under,
.viewer__wrap.is-failed > .viewer { opacity: 0; }

/* 2D: swap which layer is showing, and nothing else.
   The photograph was already in the DOM for the load and the failure, so the flat
   view costs no second request and no second decode — it is the same <img>, one
   rule further up. `is-live` hides it and `is-flat` brings it back, and the order
   matters: this comes after so it wins. */
.viewer__wrap.is-flat > .plate--under { opacity: 1; }
.viewer__wrap.is-flat > .viewer { opacity: 0; }

/* ── The 2D / 3D toggle ──────────────────────────────────────────────────────
   The same plate as the ES / EN mark: 28px square, paper on ink, inverting on
   hover and holding inverted while it is pressed. It sits in the top-right of the
   frame because that corner of a garment is the one a turntable leaves empty, and
   because the ES / EN mark is in the same corner of the page — a control you have
   already found once should be in the place your hand already goes.

   It is a BUTTON and not a checkbox because it is an action ("show me the
   photograph"), not a setting, and because the state has to survive a re-render
   that no longer has anything to do with it. */

.viewer__flat {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 2;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  background: #002FA7;
  color: var(--paper);
  font-family: var(--f-ui);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.06em;
  line-height: 1;
  transition: background 0.14s linear, color 0.14s linear;
}
.viewer__flat:hover,
.viewer__flat[aria-pressed='true'] { background: var(--ink); color: var(--paper); }
.viewer__flat:focus-visible { outline: 1px solid var(--ink); outline-offset: 2px; }

.viewer {
  position: absolute;
  inset: 0;
  background: #002FA7;
  transition: opacity 0.24s var(--ease);
  /* The canvas owns every pointer gesture in here: a drag is a rotation and a
     wheel is a dolly, and neither may reach the stage behind the panel. */
  touch-action: none;
}
.viewer__canvas {
  display: block;
  width: 100%;
  height: 100%;
  cursor: grab;
}
.viewer__canvas:active { cursor: grabbing; }
.viewer__canvas:focus-visible { outline: 1px solid var(--ink); outline-offset: -2px; }
.viewer.is-lost::after {
  content: '';
  position: absolute;
  inset: 0;
  background: #002FA7;
}

.viewer__cue {
  margin: 7px 0 0;
  font-size: 8.5px;
  letter-spacing: 0.2em;
  color: var(--grey);
  text-align: center;
}

/* ── The product head: name on the left, price on the right, one line ─────────
   The price was a separate 13px line under the title and a second copy in the
   panel header. It is now one element, set at the title's size, sharing the title's
   baseline rather than sitting beside it — two numbers of different sizes for one
   price is a thing to keep in step by hand and nothing else.

   It is NOT a flex row with `align-items: center`, which is the obvious way and
   the wrong one: a 27px display serif and a 27px display serif on the same
   baseline is the point, and centring them makes the price sit half a line lower
   than the name it belongs to. `align-items: baseline` on a flex row aligns the
   text baselines, which is what "the same line" means in type. */

.product__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin: 16px 0 0;
  flex-wrap: wrap;
}
.product__name {
  margin: 0;
  font-family: var(--f-dis);
  font-size: 27px;
  font-weight: 400;
  line-height: 1;
  letter-spacing: 0.06em;
}
.product__price {
  margin: 0;
  flex: 0 0 auto;
  font-family: var(--f-dis);
  font-size: 27px;
  font-weight: 400;
  line-height: 1;
  letter-spacing: 0.06em;
  color: var(--paper);         /* never grey: it is the price, not a caption */
  white-space: nowrap;
}

/* The blurb sits directly under the title, before anything else, because it
   describes the thing the title just named. It used to be the LAST element in the
   panel, under the add button, where reading it meant scrolling past the size
   picker. No rule and no rule above it — the head is above and the blurb is
   attached to it. */
.product__note {
  margin: 10px 0 0;
  font-family: var(--f-body);
  font-size: 12.5px;
  line-height: 1.7;
  letter-spacing: 0.01em;
  text-transform: none;   /* prose stays prose; the labels do the shouting */
}
.product__colour {
  margin: 8px 0 0;
  font-size: 10px;
  letter-spacing: 0.18em;
  color: var(--grey);
}

.label { display: block; margin: 18px 0 8px; font-size: 9px; letter-spacing: 0.24em; color: var(--grey); }

.sizes { display: flex; flex-wrap: wrap; gap: 6px; }
.size {
  min-width: 44px; height: 34px;
  padding: 0 8px;
  border: var(--hair);
  background: #002FA7;
  color: var(--paper);
  font-size: 11px;
  letter-spacing: 0.14em;
  transition: background 0.14s linear, color 0.14s linear;
}
.size:hover { background: var(--ink); color: var(--paper); }
.size[aria-pressed='true'] { background: var(--ink); color: var(--paper); }
.sizes.is-required { outline: 1px dashed var(--ink); outline-offset: 5px; }

/* ── Right tab: cart ─────────────────────────────────────────────────────── */

.lines { display: flex; flex-direction: column; gap: 16px; }

.line { display: grid; grid-template-columns: 74px 1fr; gap: 12px; align-items: start; }
.line__plate { border: var(--hair); aspect-ratio: 1; overflow: hidden; }
.line__plate img { width: 100%; height: 100%; object-fit: cover; }
.line__shot { width: 100%; height: 100%; }
.line__name { font-size: 13.8px; line-height: 1.25; letter-spacing: 0.1em; }
.line__sub { font-size: 11px; letter-spacing: 0.1em; color: var(--grey); }
.line__ctrl { display: flex; align-items: center; gap: 8px; margin-top: 8px; }

.stepper { display: flex; align-items: center; border: var(--hair); }
.stepper button {
  width: 26px; height: 26px;
  display: grid; place-items: center;
  font-size: 14px; line-height: 1;
  transition: background 0.14s linear, color 0.14s linear;
}
.stepper button:hover { background: var(--ink); color: var(--paper); }
.stepper span { min-width: 26px; text-align: center; font-size: 11px; }

.linkish {
  font-size: 9px;
  letter-spacing: 0.2em;
  color: var(--grey);
  border-bottom: 1px solid currentColor;
  padding-bottom: 1px;
}
.linkish:hover { color: var(--paper); }

.rule { border: 0; border-top: var(--hair); margin: 18px 0; }

/* Money is the one thing on the page you have to read off accurately, so it
   takes the text face rather than the tracked condensed caps the labels use.
   Shouted, but not shouting: set normal and let the row spacing do the work.
   Bold-everything at this size reads as a wall, which is the opposite of the
   point. Only TOTAL carries weight.

   Sentence-case source, uppercased here, so translations stay natural — and
   the cart and the checkout run through this same block, because a cart that
   says TOTAL and a checkout that says Total is worse than either. */
.totals {
  font-family: var(--f-body);
  font-size: 12px;
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.totals div { display: flex; justify-content: space-between; gap: 14px; padding: 7px 0; }
.totals .is-total {
  margin-top: 5px;
  padding-top: 14px;
  border-top: var(--hair);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.16em;
}
.totals .is-muted { color: var(--grey); }

/* ── Empty state ────────────────────────────────────────────────────────── */

.empty {
  height: 100%;
  min-height: 42vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  text-align: center;
}
.empty__mark { width: 46px; height: 46px; background: var(--paper); opacity: 0.9; }
.empty__title { font-size: 12px; letter-spacing: 0.24em; }
.empty__sub { font-family: var(--f-body); font-size: 12px; letter-spacing: 0.01em; color: var(--grey); text-transform: none; }

/* ── Buttons ────────────────────────────────────────────────────────────── */

.btn {
  display: block;
  width: 100%;
  margin-top: 16px;
  padding: 13px 14px;
  border: var(--hair);
  font-size: 11px;
  letter-spacing: 0.24em;
  text-align: center;
  transition: background 0.16s linear, color 0.16s linear;
}
.btn--solid { background: var(--ink); color: var(--paper); }
/* Hover INVERTS: the black plate comes up off the blue and the letter goes
   with it. It used to go to blue-on-ink, which was a hover that made the
   button look like it had stopped responding the moment the panel behind it
   turned the same blue. */
.btn--solid:hover { background: var(--paper); color: var(--ink); }
.btn--ghost { color: var(--paper); }
.btn--ghost:hover { background: var(--ink); color: var(--paper); }
.btn:disabled { opacity: 0.35; cursor: not-allowed; }

/* ── Centred windows ────────────────────────────────────────────────────── */

#scrim {
  position: fixed;
  inset: 0;
  z-index: 1400;
  background: rgba(255, 255, 255, 0.62);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  opacity: 0;
  transition: opacity 0.28s linear;
}
#scrim.is-open { opacity: 1; }

.win {
  position: fixed;
  inset: 0;
  z-index: 1500;
  display: grid;
  place-items: center;
  padding: calc(var(--head) + 8px) 16px 24px;
  pointer-events: none;
}
.win[hidden] { display: none; }

/* No border. The scrim already separates paper from scene; a hard rule around
   a floating window reads as a dialog box and this is not one. */
.card {
  pointer-events: auto;
  width: min(600px, 92vw);
  max-height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: clamp(20px, 3.4vh, 36px);
  background: #002FA7;
  color: var(--paper);
  transform: translateY(10px);
  opacity: 0;
  transition: transform 0.3s var(--ease), opacity 0.24s linear;
  scrollbar-width: thin;
}
.card--wide { width: min(880px, 95vw); }
.win.is-open .card { transform: translateY(0); opacity: 1; }

.card__kicker {
  margin: 0 0 16px;
  font-family: var(--f-dis);
  font-size: clamp(26px, 4.6vh, 40px);
  font-weight: 400;
  line-height: 1;
  letter-spacing: 0.08em;
}
.card__logo { display: block; width: 62px; height: 62px; object-fit: contain; margin: 0 auto 18px; }
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.card__prose {
  font-family: var(--f-body);
  font-size: 13px;
  line-height: 1.75;
  letter-spacing: 0.01em;
  text-transform: none;   /* a letter, not a label */
}
.card__prose p { margin: 0 0 14px; }

/* ── The poem ────────────────────────────────────────────────────────────
   Roque Dalton, in whichever language the corner toggle is set to. Three
   stanzas, so three elements, so the line breaks are real markup rather than
   escapes inside a string: a screen reader pauses at an element boundary, and a
   poem announced as one long paragraph is a poem nobody can hear properly.

   Set in the text face, centred, and not italic. Italic is the default signal
   for verse and it is wrong here, because this is not a delicate lyric. It is a
   short argument about whether difficulty should be vaulted or met, and it is
   set like a statement. */

.poem { margin: 0 0 22px; }

/* Scoped under `.poem` on purpose. `.card__prose p` above is the about panel's
   paragraph rule and it is MORE specific (one class plus a type) than a bare
   `.poem__line` (one class), so a plain class selector here loses to it and the
   poem silently gets the 14px prose gap on every line. Two classes beats one
   class plus one type, so these win without needing `!important`. */
.poem .poem__line {
  margin: 0 0 4px;
  font-family: var(--f-body);
  font-size: 12px;
  line-height: 1.65;
  color: var(--paper);
  text-align: center;
  /* No `pre-line` and no `text-wrap: balance`. The lines are separate
     paragraphs now, so they wrap the way text wraps: naturally, at whatever the
     column is, with the last line of the poem ragged. Forcing the breaks is
     what produced the blocky gaps. */
  overflow-wrap: break-word;
}
/* The last line before the attribution gives its space back, so the poem closes
   and the name sits clear of it. `:last-of-type` cannot be used here: the author
   is a <p> too, so it matched the attribution and left the final line of the
   poem carrying a gap into the gap that follows it. */
.poem .poem__body .poem__line:last-child { margin-bottom: 0; }

.poem .poem__author {
  margin: 30px 0 0;
  font-family: var(--f-ui);
  font-size: 10px;
  letter-spacing: 0.22em;
  color: var(--grey);
  text-align: center;
}

/* ── The about panel ─────────────────────────────────────────────────────
   One column, centred, in the text face, with no rules between the blocks.

   The rules and the two condensed faces were a way of separating five blocks
   that are really one short letter. They made it look like five labels rather
   than a paragraph, and the horizontal rules cut the card into rows that do not
   mean anything. So: one type face from the no-affiliation line down to the
   last word, one alignment, no furniture. The hierarchy comes from size and
   colour and from where the white space falls, which is all it ever needed. */

.card--about .card__prose {
  text-align: center;
}

/* The rule above the links is right on the checkout and the waitlist, where it
   separates a form from its action. In here it reads as one more divider in
   the middle of a letter, so the about card goes without it. */
.card--about .card__links { border-top: 0; }

.card--about .about__mark {
  font-family: var(--f-dis);
  font-size: 24px;
  line-height: 1.1;
  letter-spacing: 0.02em;
  margin-bottom: 0;
}
/* The trademark is a mark on a mark, not a word. Sized as a superscript rather
   than as a second word, which is what it is. */
.card--about .about__mark sup {
  font-family: var(--f-ui);
  font-size: 9px;
  letter-spacing: 0;
  vertical-align: super;
  margin-left: 1px;
}
.card--about .about__years {
  font-family: var(--f-body);
  font-size: 13px;
  line-height: 1.75;
  color: var(--grey);
  margin-bottom: 20px;
}
/* The no-affiliation line. The same face and the same size as everything below
   it, because everything below it is the same voice. Weight carries the little
   emphasis it needs. */
.card--about .about__disclaimer {
  font-family: var(--f-body);
  font-size: 13px;
  line-height: 1.75;
  font-weight: 600;
  margin-bottom: 14px;
}
.card--about .about__fine {
  font-family: var(--f-body);
  font-size: 13px;
  line-height: 1.75;
  color: var(--grey);
  margin-bottom: 0;
}

/* The about window signs off with where to find them, centred, same voice as
   the colophon but sized for the card. */
.card__links {
  display: flex;
  justify-content: center;
  gap: 9px;
  margin: 20px 0 0;
  padding-top: 14px;
  border-top: var(--hair);
  font-size: 9px;
  letter-spacing: 0.26em;
}
.card__links span { opacity: 0.5; }
.card__links a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color 0.14s linear;
}
.card__links a:hover,
.card__links a:focus-visible { border-bottom-color: currentColor; }

/* Only the title centres. The form stays flush left — the label-above-the-rule
   rhythm only reads as a column when everything shares one edge. */
.card--wide .card__kicker { text-align: center; }

/* The 404 card is three things and none of them is a column: the numeral, one
   line of copy, one button. Everything centres. */
#win-404 .card { text-align: center; }

/* ── Checkout ──────────────────────────────────────────────────────────────────
   The form is the one place on the site a person has to actually read and fill
   in, so it drops the condensed face for a normal text face and stops shouting.
   Bigger, looser, sentence case. */

.form,
.form .note {
  font-family: var(--f-body);
  letter-spacing: 0.02em;
  line-height: 1.5;
}

/* The fieldsets keep their grouping semantics through aria-labelledby; the
   heading is an ordinary block, because a <legend> is the one element the
   fieldset border refuses to flow around. */
.block { border: 0; border-top: var(--hair); margin: 0; padding: 22px 0 6px; }
.block:first-of-type { border-top: 0; padding-top: 0; }
.block__legend {
  margin: 0 0 16px;
  font-family: var(--f-ui);
  font-size: 10px;
  font-weight: 400;
  line-height: 1;
  letter-spacing: 0.24em;
  color: var(--grey);
}

.field { display: block; margin-bottom: 18px; }
.field__label { display: block; margin-bottom: 7px; font-size: 10px; letter-spacing: 0.18em; color: var(--grey); }
.field__input {
  width: 100%;
  padding: 10px 0;
  border: 0;
  border-bottom: var(--hair);
  border-radius: 0;
  background: transparent;
  font-family: var(--f-body);
  font-size: 15px;
  letter-spacing: 0.01em;
  text-transform: none;   /* people type addresses; shouting them back helps nobody */
}
.field__input::placeholder { color: transparent; }
.field__input:focus { outline: none; border-bottom-width: 2px; }
/* Red, but a red that reads on blue. #c00 is a white-paper error colour: at
   1.5:1 against this field it was a mark rather than a message. */
.field.is-bad .field__input { border-bottom-color: #ff8080; }
.field.is-bad::after { content: 'Required'; display: block; margin-top: 5px; font-size: 11px; letter-spacing: 0.04em; color: #ff8080; }

.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }

.choices { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 18px; }
.choice {
  flex: 1 1 auto;
  min-width: 92px;
  padding: 13px 10px;
  border: var(--hair);
  background: #002FA7;
  color: var(--paper);
  font-family: var(--f-ui);
  font-size: 12px;
  letter-spacing: 0.12em;
  transition: background 0.14s linear, color 0.14s linear;
}
.choice:hover { background: var(--ink); color: var(--paper); }
.choice[aria-checked='true'] { background: var(--ink); color: var(--paper); }
.choices--pay .choice { flex: 1 1 calc(33.333% - 6px); min-width: 104px; }

.note {
  margin: 8px 0 0;
  font-size: 12px;
  line-height: 1.65;
  letter-spacing: 0.01em;
  text-transform: none;
  color: var(--grey);
}

/* ── The purchase gate ────────────────────────────────────────────────────── */

/* The block that replaces the buy button when sales are closed. It sits in the
   same slot the button did, so the panel does not change height when the gate
   opens, which would otherwise make the page jump on every entitlement check. */
.gatebox {
  display: flex;
  flex-direction: column;
  /* Centred, because the gate is the whole panel now. It used to sit
     left-aligned against a wider panel with other things under it. With a
     button and one line of copy on their own, left-aligned reads as an
     accident rather than a decision. */
  align-items: center;
  text-align: center;
  gap: 8px;
}
.gatebox .note { max-width: 42ch; }

/* Consent. A checkbox is the one control here that must NOT be pre-ticked --
   an address stored without a recorded yes is just a list you are holding
   without permission, which is the whole thing privacy law is about. */
.check {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  margin: 10px 0 0;
  font-size: 13px;
  line-height: 1.5;
  cursor: pointer;
}
.check input {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  margin: 1px 0 0;
  accent-color: var(--ink);
  cursor: pointer;
}
.check span { min-width: 0; }

/* Honeypot. Hidden from people three ways, because any ONE of them is
   detectable: off-screen so it is not rendered, aria-hidden so a screen
   reader skips it, and tabindex="-1" in the markup so it is not focusable.
   A bot reading the DOM finds it; a person cannot reach it. */
.wl-trap {
  position: absolute !important;
  left: -9999px !important;
  width: 1px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
}

/* ── Toast ──────────────────────────────────────────────────────────────── */

#toast {
  position: fixed;
  left: 50%; bottom: 7vh;
  z-index: 2500;
  transform: translate(-50%, 8px);
  padding: 10px 16px;
  background: var(--ink);
  color: var(--paper);
  font-size: 10px;
  letter-spacing: 0.22em;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s linear, transform 0.28s var(--ease);
}
#toast[hidden] { display: none; }
#toast.is-open { opacity: 1; transform: translate(-50%, 0); }

/* ── Small screens ──────────────────────────────────────────────────────── */

/* Two thresholds and they are not the same question.
 *
 * `max-width: 640px` catches a phone held upright and anything in a narrow
 * window on a big screen. `(orientation: portrait) and (max-width: 900px)`
 * catches a tablet held upright, which is tall enough that the side panels
 * leave an unusable strip of stage. Either one means the side-by-side layout
 * has stopped working, so either one gets the sheet.
 *
 * Landscape phones are deliberately NOT in here. There is no room for a sheet
 * in a 390px-tall viewport, and the side panels are the right answer there.
 */
@media (max-width: 640px),
       (orientation: portrait) and (max-width: 900px) {

  /* How much of the viewport the sheet takes. The stage keeps the strip above
     it, which is where the room is still tappable and still legible.
     `dvh` because the mobile URL bar changes the viewport height as you scroll
     and a sheet sized in `vh` either overflows or leaves a gap. The `vh` line
     is the fallback for anything without dvh support, which is still anything
     older than 2022.

     These have to be inside a selector. A bare declaration sitting directly in
     an @media block is a parse error, and CSS error recovery does not just
     skip it -- it eats forward until it finds a brace, which took the entire
     sheet layout with it. The symptom is a media query that matches, with none
     of its rules applied, which looks exactly like a stale cache. */
  :root {
    --sheet: 74vh;
    --sheet: 74dvh;
  }

  .tab {
    /* Bottom sheet. Full width, anchored to the bottom, sliding on Y.

       `--h` is written by the drag handler and is the visitor's chosen height.
       It falls back to `--sheet`, which is the default. The two-step fallback is
       the point: with `--h` unset the sheet opens at 74%, and once it is set it
       opens where they left it. */
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    width: auto;
    height: var(--h, var(--sheet));
    /* NO max-height here, deliberately.

       The limits are owned by the drag handler, which rubber-bands past them so
       the sheet keeps responding to a fast swipe. A `max-height` in CSS wins over
       the inline `--h` the moment the rubber band goes past it, and the sheet
       stops dead at exactly the point where a fast flick was trying to move it,
       which is the lag this is all fixing.

       The bound still exists, in one place, in JavaScript, where it can be made
       to feel like resistance rather than like a wall. `overflow: hidden` below
       keeps the overshoot from painting outside the box. */
    border: 0;
    border-top: var(--hair);
    transform: translateY(100%);
    /* The home indicator and the gesture bar live here. Without this the last
       row of the cart sits under them and cannot be tapped. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
    /* A sheet that is not being dragged should never feel like it is. */
    will-change: transform;
  }

  /* Both panels rise from the same edge, so the side-specific transforms go and
     the shared one applies to both. */
  .tab--left,
  .tab--right { transform: translateY(100%); }

  .tab.is-open { transform: translateY(0); }

  /* While a finger is down the sheet must not also be animating, or the two
     fight and the sheet lags behind the gesture. The drag handler adds this. */
  .tab.is-dragging { transition: none; }

  /* The grip is live in this layout, and it is the whole affordance for
     "this can be resized". */
  .tab__grip { display: block; }

  /* The header sits under the grip, so it no longer has to make room for the
     corner marks: the sheet is at the bottom of the screen and they are at the
     top of it. */
  .tab__head { padding: 0 14px 10px; }
  .tab__title { font-size: 13px; }
  .tab__close { padding: 10px 2px; }

  /* The colophon is hidden at this size, so the clearance its absence needed is
     dead weight at the bottom of the grid. */
  .tab--left .tab__body { padding-bottom: 18px; }

  /* ── Marks go UNDER the sheet ───────────────────────────────────────────
   The marks sit at z-index 3000 and the panels at 1200, so a sheet was being
   drawn over the top of the header. On a wide screen that never comes up
   because the panels are at the sides. Here they share the middle of the
   screen and the marks hung over the panel like furniture in front of a
   television.

   Dropping them below the sheet is the fix. The sheet then owns the bottom of
   the screen outright, which is what a bottom sheet is: a layer, not a
   window. They come back the moment it closes, because this is scoped to
   `[data-panel]`, which store.js only sets while one is open. */
  [data-panel] .mark,
  [data-panel] .tally,
  [data-panel] .lang__menu { z-index: 1100; }

  /* The about mark is the one that stays up and stays put.

     It is the ONLY route to the about panel anywhere in the site, and at the
     bottom-left of a phone that is underneath a sheet covering three quarters
     of the screen. So it keeps a z-index above the panel rather than going under
     with the rest, and it does not move: bottom-left of the viewport, always, at
     the same place whether a sheet is open or not.

     It used to ride up to sit on the stage strip while a panel was open, and
     that was worse than the problem it solved. A control that changes corner
     depending on what else is open is a control you have to hunt for, and
     tracking it with your eye while a panel is in the way is exactly the work a
     sheet is supposed to be saving you. Fixed position, always findable. */
  [data-panel] .mark--bl {
    z-index: 1300;
    bottom: 16px;
  }

  /* ── Containing the sheet ──────────────────────────────────────────────
     `contain` is what stops a resize from costing a full-document layout. The
     sheet holds its own box, so changing its height reflows the panel and
     nothing behind it - which is the whole reason the drag tracks the finger
     now instead of stuttering a frame behind it. `overflow: hidden` stops the
     content poking out of the top while the height is in motion. */
  .tab {
    contain: layout paint;
    overflow: hidden;
  }

  /* ── No button outlines on a phone ────────────────────────────────────
     A 1px border around a button on a 390px screen reads as a highlight box
     rather than as a control, and a product page has five of them at once
     (the size row alone is four), which is what makes it look like a form
     instead of a shop.

     The fill does the work instead. Solid is solid; everything else gets a flat
     tint and goes solid when it is selected, which is what a borderless
     iOS-style button uses to say "this is tappable" without drawing a box
     round it. Selected state is unchanged - it was already an inversion - so
     nothing moves here but the tint and the border.

     The tint is white at 18%, not a flat #f1f1f1. A plate of near-white paper
     on the Klein blue is a control that looks like it belongs to a different
     site; a lift of the field's own colour says "this is a target" without
     introducing a second surface. */
  .btn, .choice, .size, .swatch, .tab__close, .lang__opt { border: 0; }

  .btn--ghost, .choice, .size, .swatch { background: rgba(255, 255, 255, 0.18); }
  .btn--ghost:hover { background: var(--ink); color: var(--paper); }
  .btn:active, .choice:active, .size:active, .swatch:active { opacity: 0.72; }

  .choice[aria-checked='true'],
  .size[aria-pressed='true'],
  .swatch[aria-pressed='true'] { background: var(--ink); color: var(--paper); }

  .tab__close { padding: 12px 10px; font-size: 15px; }
  .lang__opt[aria-checked='true'] { background: var(--ink); color: var(--paper); }

  /* Hover means nothing on a screen with no hover. The marks are inverted on
     touch the moment they are tapped, and stay that way, so every hover rule
     on them is a rule that turns a tap into a permanent state change.

     For the language mark that rule is "the edge stays lit while the menu is
     open", which is the same rule its `[aria-expanded]` state already states -
     the hover blink would otherwise leave its outline painted over a mark
     nobody is pointing at. The press is not cancelled here: `.mark--lang:active`
     is a tap, and a tap is exactly what this rule is protecting. */
  .mark:hover .mark__edge,
  .mark--lang:hover { animation: none; }
  .mark--lang:hover { outline-color: transparent; }
  .mark--lang[aria-expanded='true']:hover { outline-color: currentColor; }
  .mark:hover .mark__fill { transform: none; }

  /* While a sheet is up the stage strip is the only live part of the page, so
     the top marks stay reachable and the close button is not the only way out.
     Everything else stays as it was. */
  .field-row { grid-template-columns: 1fr; }
  .win { padding-left: 10px; padding-right: 10px; }
  .card { padding: 18px 16px; }

  /* Windows stay centred and full-bleed as they are on desktop. About,
     checkout, order, the waitlist and the in-app 404 are moments, not panels,
     and a moment should not arrive as a drawer. */
  .win { padding: calc(var(--head) + 8px) 10px 24px; }
}

/* ── Landscape phones ────────────────────────────────────────────────────────
   Below this the sheet would leave too little stage to be worth having, so a
   phone on its side gets the side panels back. */
@media (orientation: landscape) and (max-height: 520px) {
  :root { --tab: min(58vw, 340px); --sheet: 100vh; --sheet: 100dvh; }
  .tab {
    top: 0;
    bottom: 0;
    left: auto;
    right: auto;
    width: var(--tab);
    height: 100%;
    border-top: 0;
    transform: none;
  }
  .tab--left {
    left: 0;
    right: auto;
    border-right: var(--hair);
    transform: translateX(-100%);
  }
  .tab--right {
    right: 0;
    left: auto;
    border-left: var(--hair);
    transform: translateX(100%);
  }
  .tab.is-open { transform: translateX(0); }
  .tab__grip { display: none; }
  .tab__head { padding: 8px 14px 8px; }
  [data-panel] .mark--bl { bottom: 16px; }
  .colophon { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .tab, .card, #toast, #scrim, .mark__fill, .mark__ink { transition-duration: 0.01ms; }
  .mark--bl { transition: none; }
  .mark:hover .mark__edge { animation: none; opacity: 1; }
  #scrim { -webkit-backdrop-filter: none; backdrop-filter: none; }
}

/* — The diagnostics panel ------------------------------------------
   Only ever built when the URL has ?debug=1, so this is dead weight otherwise.
   Fixed and low, out of the way of the header marks, and it must never be the
   thing that is hard to tap on a phone in the middle of a test. */
.dg {
  position: fixed;
  left: 8px;
  right: 8px;
  bottom: 8px;
  z-index: 9999;
  background: #0b0b0f;
  color: #d8d8d8;
  border: 1px solid #333;
  font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
}
.dg__bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border-bottom: 1px solid #26262e;
}
.dg__title { letter-spacing: 0.18em; color: #8a8a96; }
.dg__count {
  min-width: 22px;
  text-align: center;
  padding: 1px 5px;
  background: #26262e;
  color: #e8e8ee;
}
.dg__btn {
  margin-left: auto;
  padding: 3px 9px;
  font: inherit;
  letter-spacing: 0.08em;
  color: #e8e8ee;
  background: #26262e;
  border: 1px solid #3a3a46;
  cursor: pointer;
}
.dg__btn + .dg__btn { margin-left: 0; }
.dg__list {
  max-height: 34vh;
  overflow: auto;
  padding: 6px 8px;
  -webkit-overflow-scrolling: touch;
}
.dg__row { padding: 2px 0; border-bottom: 1px solid #1b1b22; word-break: break-word; }

/* The heading that separates a recovered log from the live one. A log that looks
   identical either way cannot be read: you cannot tell which crash you are
   looking at. */
.dg__head {
  padding: 5px 0 3px;
  letter-spacing: 0.14em;
  color: #d8b45a;
  border-bottom: 1px solid #3a3320;
}
.dg__head--now {
  margin-top: 6px;
  color: #8a8a96;
  border-bottom-color: #26262e;
}
