/*
 * Pazuru on the web — marketing, privacy, terms.
 *
 * The tokens are the app's, transcribed from client/mobile/src/theme/tokens.ts
 * and design/TOKENS.md. Do not "improve" them here: the site and the app are
 * meant to read as one object, and a value that drifts on the web is a value
 * somebody will later copy back into the app.
 *
 * SINGLE THEME, DELIBERATELY. The product is a dark green gallery wall with
 * pictures hung on it. There is no light mode to design, so every colour is
 * painted explicitly rather than inherited from the browser.
 *
 * BRASS CHANGES VALUE WHEN IT CROSSES GROUNDS. #C08F4E on the wall, #7E5C28
 * on paper. LOAD-BEARING.md is emphatic: the wall value on paper "reads as a
 * sticker". Both are here as separate tokens so the choice has to be made.
 */

:root {
  --wall: #232D28;
  --wall-top: #2B3630;
  --wall-bottom: #212B26;
  --mat: #CFC9BC;
  --paper: #E7E2D6;
  --paper-card: #FFFDF7;
  --frame: #141813;
  --well: #20251F;

  --brass: #C08F4E;          /* on the wall */
  --brass-ink: #7E5C28;      /* on paper — never #C08F4E here */
  --on-brass: #1C231D;

  --ink1: #EDEADF;
  --ink2: #C2CCC2;
  --ink2i: #B3BDB2;
  --ink3: #93A096;
  --ink4: #7D8A80;

  --pink1: #1D231E;          /* ink on paper */
  --pink3: #6C6A5C;

  --hair: rgba(237,234,223,.14);
  --hair-soft: rgba(237,234,223,.10);
  --hair-strong: rgba(237,234,223,.22);

  --serif: Literata, Georgia, "Times New Roman", serif;
  --sans: Karla, system-ui, -apple-system, sans-serif;
  --mono: "DM Mono", ui-monospace, "SF Mono", Menlo, monospace;

  --gutter: 22px;
  --measure: 62ch;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  background: var(--wall);
  color: var(--ink2);
  font: 400 15px/1.65 var(--sans);
  -webkit-font-smoothing: antialiased;
  position: relative;
}

/* The wall's glow, as a fixed layer rather than `background-attachment:
   fixed`. That property is unreliable on iOS Safari — which is most of this
   page's traffic — and forces a full repaint on every scroll frame. A fixed
   pseudo-element behind everything gets the same look and composites once. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background: radial-gradient(ellipse at 50% 0%, var(--wall-top) 0%, var(--wall-bottom) 62%);
}

/* `height: auto` is load-bearing, not tidiness: the plates carry width/height
   attributes so the layout does not jump while they load, and without this a
   `width: 100%` image renders at its attribute height — square plates come
   out as tall strips. */
img { max-width: 100%; height: auto; display: block; }

a { color: var(--brass); text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { color: var(--ink1); }
a:focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 3px;
  border-radius: 2px;
}

.shell {
  width: 100%;
  max-width: 1000px;
  margin: 0 auto;
  padding: 0 var(--gutter) 72px;
}

/* --- the wordmark, and the rule that always sits under it ---------------- */

.masthead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 30px 0 0;
  flex-wrap: wrap;
}

.wordmark {
  font: 600 24px/1 var(--serif);
  letter-spacing: .008em;
  color: var(--ink1);
  text-decoration: none;
}
.wordmark:hover { color: var(--ink1); }

.masthead nav {
  display: flex;
  gap: 20px;
  font: 700 9.5px var(--sans);
  letter-spacing: .18em;
  text-transform: uppercase;
}
.masthead nav a { color: var(--ink3); text-decoration: none; }
.masthead nav a:hover, .masthead nav a[aria-current] { color: var(--brass); }

.rule { height: 1px; background: rgba(192,143,78,.5); margin-top: 14px; }

/* --- small caps, in the app's two sizes ---------------------------------- */

.field {
  font: 700 9.5px var(--sans);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink3);
}

.section-label {
  font: 700 12px var(--sans);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--brass);
}

/* --- the mount: frame, rebate, mat, opening, label ----------------------- */
/* The signature object. Same proportions as COMPONENTS.md section 1: 4px
   rebate, 24px mat sides and top, a WIDER bottom band carrying the label.
   Do not equalise the bottom — that is where a real mount carries its
   caption, and it is the whole reason the object reads as a mount. */

.mount {
  background: var(--frame);
  border: 1px solid rgba(192,143,78,.5);
  border-radius: 2px;
  padding: 4px;
  box-shadow: 0 22px 48px rgba(0,0,0,.55);
}
.mount .mat {
  background: var(--mat);
  padding: 24px 24px 26px;
  position: relative;
}
.mount .opening {
  aspect-ratio: 1;
  background: var(--well);
  overflow: hidden;
  box-shadow: inset 0 1px 3px rgba(0,0,0,.35);
  /* The sliced board inside is absolutely positioned against this. */
  position: relative;
  /* And the size the tile numerals scale against. The app's badge is 27px on
     a 350px board; expressing that as a share of the container keeps the
     proportion at any width instead of pinning it to the desktop mount. */
  container-type: inline-size;
}
.mount .opening img { width: 100%; height: 100%; object-fit: cover; }
.mount .label { padding-top: 22px; }
.mount .label .title {
  font: italic 500 15px/1.3 var(--serif);
  color: var(--pink1);
}
.mount .label .meta {
  margin-top: 6px;
  font: 700 9.5px var(--sans);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--pink3);
}

/* --- a picture, sliced --------------------------------------------------- */
/* The page's job is to make it obvious in one glance that this is a SLIDING
   puzzle, so the hero shows a real board rather than a photograph of a frame:
   nine tiles cut from one plate, one of them missing, and the gap dark enough
   to read as a hole rather than as a dark tile.
 *
 * Each tile is the whole plate at 300% with its own background-position, so
 * the nine of them reassemble into the original picture pixel for pixel and
 * ONE image is downloaded, not nine. Position is a transform, never a grid
 * cell — grid placement does not animate and transforms composite on the GPU.
 *
 * The scrambled arrangement is written into the HTML as inline transforms.
 * That is not laziness: it means the board is already a correct scrambled
 * puzzle with no JavaScript, and the script only ever moves it. */

.sliced { position: absolute; inset: 0; }

/* WHICH PICTURE, AS A PLAIN CLASS. This was a `var(--art)` set inline, which
   is one more thing that has to work before any artwork appears at all — and
   when it does not, the mount renders as an empty dark rectangle rather than
   as a degraded picture. A stylesheet rule fails in no browser. */
.sliced.art-woodblock span  { background-image: url(/plates/woodblock.webp); }
.sliced.art-still-life span { background-image: url(/plates/still-life.webp); }
.sliced span {
  position: absolute;
  top: 0;
  left: 0;
  /* 1/3 with a hair over, so the seams between tiles never open up on a
     fractional device pixel and show the well through the picture. */
  width: 33.34%;
  height: 33.34%;
  background-size: 300% 300%;
  background-repeat: no-repeat;
  box-shadow: inset 0 0 0 1px rgba(20,24,19,.40);
  transition: transform .26s cubic-bezier(.2,.7,.3,1),
              opacity .55s ease,
              box-shadow .55s ease;
}

/* THE GAP IS A TILE, not an absence — the same model the app's Board.tsx
   uses (`opacity: isGap && !solved ? 0 : 1`). It slides with every move like
   any other tile, invisibly, and reappears the instant the board is solved.
   Without it a sliding puzzle can never show its own picture finished, and a
   page headlined "put it back together" would never put anything back. */
.sliced .gap { opacity: 0; }
.sliced.solved .gap { opacity: 1; }

/* Solved, the seams go too. A finished puzzle is a picture again. */
.sliced.solved span { box-shadow: none; }

@media (prefers-reduced-motion: reduce) {
  /* The static scramble IS the message. Reduced motion keeps the board and
     drops only the solve, so nothing is lost but the movement. */
  .sliced span { transition: none; }
}

/* --- the board, played --------------------------------------------------- */
/* THE DEMONSTRATION COMES FIRST AND THE TOY SECOND. The board solves itself
   on the way in, because the payoff of a page headlined "Art worth putting
   back together" is the picture becoming whole; only then does it offer to be
   played. A board a visitor abandons mid-scramble would spend that payoff.
 *
 * Play is a pointer-and-keyboard affordance on a screen with room for it —
 * script gates it at 800px, matching the breakpoint where the hero stops
 * being two columns. Below that the mount can be 180px tall (see the fold
 * arithmetic further down), which makes a 3x3 tile 58px: under Apple's 44px
 * minimum only just, and a fiddly thing to aim at beside a headline. The
 * demonstration is the better use of that space, and it is what stays. */

/* Instant, for the shuffle. Twenty animated moves would be five seconds of
   flurry before the visitor gets a turn; the scramble is a starting position,
   not a performance. Removed again on the next frame so playing animates. */
.sliced.instant span { transition: none; }

.sliced.playable span { cursor: pointer; }
/* Only the eight that can be picked up. The gap is not a target. */
.sliced.playable .gap { cursor: default; }

/* --- the app's cues, at the app's values --------------------------------- */
/* THIS IS WHAT MAKES A BOARD LOOK TOUCHABLE. Straight from the app's
   tokens.ts and Board.tsx: a tile that can move carries a brass hairline, the
   one just moved a brighter and thicker one, and everything else the faintest
   possible light edge. Without them the board is a picture of a puzzle; with
   them it is obvious where the next move is. Only while playing — the app
   drops every cue on solve (`cue = solved ? undefined : …`) and so does this,
   because `playable` comes off at the same moment. */

.sliced.playable span {
  box-shadow: inset 0 0 0 1px rgba(20,24,19,.40),
              inset 0 0 0 .5px rgba(255,255,255,.05);
  /* THE CUE MUST NOT FADE IN. The base rule transitions box-shadow over .55s,
     which is right for the solve — the seams dissolving as the picture becomes
     whole — and wrong for a cue: half a second is long enough that the brass
     arrives after you have already looked for it, on every single move. Only
     the slide is animated while playing. The .55s comes back on solve, when
     `playable` comes off, which is exactly when it is wanted. */
  transition: transform .26s cubic-bezier(.2,.7,.3,1);
}
.sliced.playable span.movable {
  box-shadow: inset 0 0 0 1px rgba(20,24,19,.40),
              inset 0 0 0 1px rgba(192,143,78,.34);
}
.sliced.playable span.last {
  box-shadow: inset 0 0 0 1px rgba(20,24,19,.40),
              inset 0 0 0 1.5px rgba(192,143,78,.62);
}

/* --- the numerals -------------------------------------------------------- */
/* A pseudo-element, so nine tiles stay nine nodes and the numeral cannot be
   selected or dragged off the picture. Values are the app's: a #171B18 chip
   with a 0.75px paper hairline — LOAD-BEARING.md 1 is emphatic that it is
   0.75 and not 1 or 0.5 — the drop shadow first and the hairline second,
   because the order of a shadow list is what puts the paper edge on top.
 *
 * The px values are fallbacks for a browser without container queries; the
 * cqw ones are what actually apply. 7.7cqw is the app's 27/350 and 5.4cqw its
 * 19/350, so the chip lands where it does on a phone. */
.sliced.numbers.playable span::before {
  content: attr(data-tile);
  position: absolute;
  top: 4px;
  left: 4px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 5px;
  min-width: 25px;
  min-width: 7.7cqw;
  height: 25px;
  height: 7.7cqw;
  border-radius: 3px;
  background: #171B18;
  color: #EFEADC;
  font: 500 18px/1 var(--mono);
  font-size: 5.4cqw;
  letter-spacing: -.3px;
  box-shadow: 0 1px 3px rgba(0,0,0,.45),
              inset 0 0 0 .75px rgba(228,222,206,.82);
}

/* Recede when placed. LOAD-BEARING.md 2 names the 0.8, and names it as a
   shipped default rather than an option — so it is not a variable here
   either. `settled` is about being HOME, not about being movable. */
.sliced.numbers.playable span.settled::before { opacity: .8; }

/* The hole has no numeral, and neither has a finished picture. */
.sliced.numbers.playable .gap::before { content: none; }

/* The board takes focus as one control, not nine: a sliding puzzle is a
   single widget whose state is the arrangement, and nine tab stops would be
   nine stops with nothing to say. Arrow keys move from here. */
.sliced.playable:focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 4px;
}

/* THESE GUARDS ARE LOAD-BEARING, and one of them shipped broken once.
   `display: flex` on the same element as the `hidden` attribute beats the
   browser's own `[hidden] { display: none }` — author styles win over the UA
   stylesheet regardless of specificity — so without these the controls are
   visible while the board cannot be played, and visible with no JavaScript at
   all, which is the one thing shipping them hidden was for.
 *
 * `.play-entry` is here for the same reason at one remove: it takes
 * `.button`, which is `inline-flex`, so it inherited the bug the moment it
 * stopped being a bare text label — and it showed the brass CTA underneath a
 * game already in progress. ANY element in here that is hidden from script
 * and styled with display needs its own line. */
.play[hidden],
.play-actions[hidden],
.play-entry[hidden] { display: none; }

.play { margin-top: 14px; }

/* Hidden but still occupying its space — 52px, which is what this block holds
   in every state: the brass button before a game, the three cells during one.
   Together with the stats band's 56px above the frame, that makes the whole
   figure column a fixed height, which is what the centred text beside it
   depends on. It also means the offer arrives without shifting the page.
 *
   `visibility` rather than `display` keeps the box while taking the contents
   out of the tab order and the accessibility tree, which is what "not yet
   offered" should mean. Scoped to the class so a browser with no JavaScript
   reserves nothing: there the offer never comes, and the space would be
   111px of wall held open for something that will never fill it. */
.play.reserved[hidden] {
  display: block;
  visibility: hidden;
}


/* --- arriving ------------------------------------------------------------ */
/* AN ANIMATION, NOT A TRANSITION, and the difference is the whole reason this
   works. These three take `display: none` between states, and a transition
   cannot interpolate out of that — the element would simply appear. A keyframe
   animation runs when an element STARTS being rendered, which is exactly the
   moment in question, and it restarts each time, so the row fades in every
   time a game begins.
 *
   The default is `opacity: 1`, so a browser that ignores the animation shows
   the controls rather than hiding them. That direction matters: the failure
   mode of the other way round is an invisible board with invisible controls. */
@keyframes play-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.play-actions,
.play-entry {
  animation: play-fade-in .28s ease;
}

@media (prefers-reduced-motion: reduce) {
  .play-actions,
  .play-entry {
    animation: none;
  }
}

/* --- the stats band ------------------------------------------------------ */
/* The app's, above the board as it is there: TIME at one end, MOVES at the
   other, `min-height` so the band is the same height holding two figures as
   holding nothing. That last part is not tidiness — it is what stops the
   picture dropping 68px when a game starts, and what lets the text beside it
   be centred at all.
 *
 * Hidden with `visibility` rather than `display`, so the height is kept and
 * the contents stay out of the accessibility tree; and because the element is
 * rendered throughout, a plain opacity transition can do the fade, where the
 * controls below need a keyframe animation to fade out of `display: none`. */
.stats {
  display: flex;
  justify-content: space-between;
  align-items: center;
  min-height: 56px;
  margin-bottom: 12px;
  padding: 0 2px;
  opacity: 0;
  visibility: hidden;
  transition: opacity .28s ease;
}
.stats.live {
  opacity: 1;
  visibility: visible;
}
.stats-moves { text-align: right; }

/* 27px DM Mono, tabular so a running clock does not jitter the band on every
   tick, and 4px under its label — the app's `statValue` exactly. */
.stat-value {
  font: 500 27px/1 var(--mono);
  color: var(--ink1);
  margin-top: 4px;
  font-variant-numeric: tabular-nums;
}

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

/* --- the way in --------------------------------------------------------- */
/* A REAL BUTTON, and the page's own: `.button.primary` supplies the brass
   fill, the 52px and the 3px radius, so this only has to say how wide it is.
   It was a small-caps text label, which was too quiet to read as the thing
   that starts a game.
 *
 * Full width of the mount for two reasons. It is the more obvious of the two,
 * and — the one that decided it — the action row that replaces it is exactly
 * this wide, so the block does not reflow when the game starts. A button that
 * resizes the column underneath it as you press it feels broken.
 *
 * On brass twice in one hero: the App Store control is Apple's black badge,
 * so nothing else in the hero is spending the brass fill, and this sits under
 * the frame rather than beside the badge. If the two ever do compete, this is
 * the one to make an outline. */
.play-entry {
  width: 100%;
  cursor: pointer;
  border: 0;
}

/* --- the app's ActionRow ------------------------------------------------- */
/* Equal thirds, 52px, radius 3, 8px gap, a stacked label and price — the
   shape of Controls.tsx, because somebody who has played the app should
   recognise this row and somebody who has not should learn the app from it.
   Undo keeps the brass wash: COMPONENTS.md 3 names that as the reason it
   reads as the most-pressed control. */

.play-actions {
  display: flex;
  gap: 8px;
}
.play-actions button {
  flex: 1;
  min-height: 52px;
  padding: 6px 4px;
  border-radius: 3px;
  border: 1px solid var(--hair-strong);
  background: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  font: 700 11px var(--sans);
  color: var(--ink1);
  cursor: pointer;
}
.play-actions .cell-sub {
  font: 400 9.5px var(--sans);
  letter-spacing: .04em;
  color: var(--ink4);
}
.play-actions button:hover { border-color: var(--brass); }
.play-actions button:focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 2px;
}

/* Undo's price is on the button and never in a confirm — the rule
   COMPONENTS.md 3 is actually protecting. */
.play-undo {
  border-color: rgba(192,143,78,.6) !important;
  background: rgba(192,143,78,.10) !important;
}
.play-undo:disabled {
  border-color: rgba(237,234,223,.12) !important;
  background: rgba(237,234,223,.03) !important;
  color: var(--ink4);
  cursor: default;
}
.play-undo:disabled .cell-sub { color: rgba(125,138,128,.6); }

/* On, in brass. Off, in the same grey as every other sub-label — the state is
   the word, never a glyph. */
.play-numbers[aria-pressed="true"] .cell-sub { color: var(--brass); }

/* For screen readers only. The visible status carries a move count that
   changes on every slide, which a live region would narrate into noise; this
   one speaks twice — at the start and on the solve. */
.at-once {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --- how it works -------------------------------------------------------- */
/* ONE PICTURE, THREE TIMES, SMALL. The heading is already the sentence, so
   these carry no captions of their own — they are the same still life picked,
   gridded and slid, read left to right like a diagram on a wall label. Three
   captioned full-width artworks said the same thing and took over the page;
   the gameplay is a footnote to the artwork, not the other way round. */

.steps {
  margin-top: 24px;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: 420px;
}
.step {
  /* Shrink to fit a phone, cap on a desktop. Fixed widths overflow at 375px
     and fixed percentages get silly on a wide screen. */
  flex: 1 1 0;
  min-width: 0;
  max-width: 118px;
  background: var(--frame);
  border: 1px solid rgba(192,143,78,.35);
  border-radius: 2px;
  padding: 3px;
  box-shadow: 0 6px 18px rgba(0,0,0,.32);
}
.step .stage {
  position: relative;
  aspect-ratio: 1;
  background: var(--well);
  overflow: hidden;
}
.step .stage img { width: 100%; height: 100%; object-fit: cover; }

.steps .arrow {
  flex: none;
  color: var(--brass);
  font: 400 14px var(--sans);
  line-height: 1;
}

/* The middle frame: the same picture under the grid you would play it on. */
.stage.gridded::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(to right, rgba(231,226,214,.7) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(231,226,214,.7) 1px, transparent 1px);
  background-size: 25% 25%;
}

/* --- special thanks ------------------------------------------------------ */

.institutions {
  font: 600 17px/1.75 var(--serif);
  color: var(--ink1);
  max-width: var(--measure);
}

/* --- hero ---------------------------------------------------------------- */

/* THE HERO IS THE PAGE'S ONE BIG GESTURE and everything below it is small.
   Give it the room: a section that follows it starts 58px later regardless,
   so the only place the picture can get its scale from is here. */
.hero {
  display: grid;
  grid-template-columns: 1fr;
  gap: 44px;
  padding: 62px 0 28px;
  align-items: center;
}
@media (min-width: 800px) {
  /* 48 at the foot, not 36: the figure column is 134px taller than it was
     before it carried a stats band and a control row, and the hero was
     sitting closer to the section under it than the composition wants. This
     is breathing room and nothing more — what stops the text moving is the
     fixed column height, not the padding. */
  .hero { grid-template-columns: 1fr 360px; gap: 56px; padding: 80px 0 48px; }
  /* CENTRED, WHICH IS ONLY SAFE BECAUSE THE RIGHT COLUMN NO LONGER CHANGES
     HEIGHT. Centring makes the text's position a function of how tall the
     figure column is, and that is exactly why the headline used to drop 47px
     when the controls appeared. It is stable now for one reason: the stats
     band holds its 56px whether it is showing anything or not, and the
     control row below the frame is 52px whether it holds the button or the
     three cells. Nothing in that column moves, so nothing beside it can.
     Break either of those and the headline starts sagging again. */
  .hero { align-items: center; }
}
@media (min-width: 1000px) {
  .hero { grid-template-columns: 1fr 390px; gap: 64px; }
}

/* text-wrap: balance is doing real work here — the headline is three words
   long and wants to break after "worth", not after "back". */
.hero h1 {
  font: 600 42px/1.1 var(--serif);
  color: var(--ink1);
  text-wrap: balance;
  letter-spacing: -.005em;
}
@media (min-width: 800px)  { .hero h1 { font-size: 50px; } }
@media (min-width: 1000px) { .hero h1 { font-size: 56px; } }

.hero .stand {
  margin-top: 20px;
  font: 400 18px/1.6 var(--sans);
  color: var(--ink2);
  max-width: 42ch;
}

.hero .note {
  margin-top: 20px;
  font: 400 13px/1.6 var(--sans);
  color: var(--ink4);
  max-width: 46ch;
}

.hero .availability { margin-top: 18px; }

/* --- the hero on a phone -------------------------------------------------- */
/* ONE REQUIREMENT: the whole mount — frame, artwork and caption — has to be
   above the fold on a 375x812 screen, where it previously overflowed by
   201px. Everything here is scoped under the desktop breakpoint so the
   desktop hero is bit-for-bit unchanged.
 *
 * The 201px comes back in three places, in this order of preference:
 *   1. Spacing that only exists because it was tuned on a wide screen.
 *   2. The headline, which at 42px broke to three lines in a 331px column
 *      and at 38px breaks to two — smaller type, but LESS vertical space,
 *      and a better break.
 *   3. The card itself, capped and centred, only for what is still missing.
 *
 * The cap is a max-width, not a transform: the mat and label scale with it,
 * so the artwork is never cropped and nothing is distorted. */

@media (max-width: 799px) {
  /* NO BAND ON A PHONE. No turn is offered below 800px, so there is nothing
     to count and nothing to hold space for — and its 56px plus 12px of margin
     is exactly what the fold cannot spare: it put the mount at 549 to 856 on
     a 375x812 screen, overflowing by 44px and breaking the one requirement
     the arithmetic below exists to hold. */
  .stats { display: none; }

  .hero { padding: 28px 0 24px; gap: 18px; }
  /* 36, not 38: "Art worth putting" measures 317px at 36 and 334px at 38, in
     a 331px column. Two lines instead of three — 46px back, and a break that
     falls where the phrase does. */
  .hero h1 { font-size: 36px; }
  .hero .stand { margin-top: 16px; font-size: 16.5px; }
  .hero .note { margin-top: 16px; }
  .cta-row { margin-top: 20px; }
  /* 14, not 12: the badge above it wants 13px of clear space. */
  .hero .availability { margin-top: 14px; }

  /* THE CTA ROW MUST NOT WRAP HERE, and it is arithmetic rather than taste.
     155.56 (badge) + 14 (gap) + 151 ("See the artwork") = 320.56 in a 331px
     column, with 10px to spare. A second row costs 62px, and 62px is
     precisely the budget the mount is sized out of below — the 565 constant
     is 481 + 72 + 12, and the 481 is everything above the card measured
     against a single row. A text button reading "Download on the App Store"
     was 227px and did wrap, which is what sent the mount to 850 on a 375x812
     viewport and broke the one requirement this block exists to hold.
     Measured with the badge: mount top 479, bottom 786.

     SO: if either label or the badge size changes, measure that again. */

  /* width: 100% is load-bearing. A grid item with auto inline margins is
     sized to fit-content, not stretched — without it the mount collapses to
     the width of its own caption (236px) and max-width never applies. */
  .hero .mount { width: 100%; max-width: min(300px, 72vw); margin-inline: auto; }

  /* AND AGAINST HEIGHT, because a phone's real viewport is not its screen.
     An emulator gives a 390-wide iPhone 844px; Safari with its URL bar showing
     gives 745, and the card that fits the first overflows the second by 67px.
     svh is the URL-bar-visible height — the one the page actually loads into —
     so sizing against it is what makes "visible on load" true on a device.
     Deliberately not dvh: the card must not resize while the bar collapses.

       565 = 481 (everything above the card) + 72 (frame, mat and caption,
             which are fixed px and do not scale with the opening) + 12 clear

     72, not 61: below about 220px of card the artist-and-year line wraps to
     two, and a constant measured on the unwrapped case leaves 2px of margin
     on exactly the viewport this rule exists for.

     The 180px floor is an admission: on a viewport short enough to bind it
     (an iPhone SE at 667) nothing fits either way, and a card scaled to the
     arithmetic would be 123px of unreadable print. Declared separately so a
     browser without svh keeps the width-only cap above rather than dropping
     both. */
  .hero .mount { max-width: min(300px, 72vw, max(180px, 100svh - 565px)); }
}

/* --- buttons ------------------------------------------------------------- */
/* ONE SOLID BRASS FILL PER VIEW. Two competing primaries read as a mistake,
   and the radius is 3 because that is the product's only button radius. */

/* gap: 14px is Apple's, not ours. The badge wants clear space of a quarter
   its height on every side, and a quarter of 52 is 13. */
.cta-row { margin-top: 26px; display: flex; gap: 14px; flex-wrap: wrap; }

/* THE BADGE IS 119.66x40 AND MUST NOT BE RESTYLED — height only, width from
   its own ratio, which is 155.56px at the 52px the buttons beside it stand.
   The img attributes are 120x40 for the aspect ratio alone; they reserve the
   space before the SVG arrives and are never the rendered size. */
.badge { display: inline-flex; align-items: center; }
.badge img { display: block; height: 52px; width: auto; }

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 52px;
  padding: 0 24px;
  border-radius: 3px;
  font: 700 14px var(--sans);
  text-decoration: none;
}
.button.primary { background: var(--brass); color: var(--on-brass); }
.button.primary:hover { background: #CB9C5C; color: var(--on-brass); }
.button.secondary {
  border: 1px solid var(--hair-strong);
  color: var(--ink1);
  font-weight: 500;
}
.button.secondary:hover { border-color: var(--brass); color: var(--ink1); }

/* --- sections ------------------------------------------------------------ */

section { padding-top: 58px; }
section > .section-label + h2, section > .section-label + p { margin-top: 14px; }

h2 {
  font: 600 27px/1.25 var(--serif);
  color: var(--ink1);
  text-wrap: balance;
}
h3 { font: 600 17px var(--serif); color: var(--ink1); }

.prose { max-width: var(--measure); }
.prose p { margin-top: 14px; color: var(--ink2); }
.prose p:first-child { margin-top: 0; }

/* --- the collections shelf ----------------------------------------------- */

.shelf {
  margin-top: 26px;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 18px 16px;
}
@media (min-width: 620px) { .shelf { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 860px) { .shelf { grid-template-columns: repeat(4, 1fr); } }

.plate .frame {
  background: var(--frame);
  border: 1px solid rgba(192,143,78,.35);
  border-radius: 2px;
  padding: 3px;
  box-shadow: 0 6px 18px rgba(0,0,0,.32);
}
.plate .frame img { aspect-ratio: 1; object-fit: cover; width: 100%; }
.plate .name {
  margin-top: 9px;
  font: 600 15px/1.25 var(--serif);
  color: var(--ink1);
}
.plate .count {
  margin-top: 4px;
  font: 500 10.5px var(--mono);
  color: var(--ink4);
  font-variant-numeric: tabular-nums;
}

/* --- price ---------------------------------------------------------------- */

.prices {
  margin-top: 24px;
  display: grid;
  gap: 14px;
  grid-template-columns: 1fr;
}
@media (min-width: 640px) { .prices { grid-template-columns: 1fr 1fr; } }

.price {
  border: 1px solid var(--hair);
  border-radius: 3px;
  padding: 22px;
  background: rgba(237,234,223,.02);
}
.price .amount {
  font: 500 30px var(--mono);
  color: var(--ink1);
  font-variant-numeric: tabular-nums;
}
.price h3 { margin-top: 8px; }
.price p { margin-top: 8px; font-size: 13.5px; color: var(--ink3); }

/* --- documents (privacy, terms) ------------------------------------------ */

/* A DOCUMENT PAGE NARROWS THE WHOLE SHELL, not just the text.
   A 62ch column left-aligned inside the full-width masthead orphans a third
   on the right and reads as a mistake rather than as a measure. Bringing the
   masthead, rule, text and footer to one width fixes it without stretching
   prose to an unreadable line length — the column stays ~75 characters. */
.shell.doc-page { max-width: 700px; }

.doc { padding-top: 46px; max-width: none; }
.doc h1 { font: 600 36px/1.15 var(--serif); color: var(--ink1); }
.doc .updated { margin-top: 10px; font: 500 11px var(--mono); color: var(--ink4); }

.doc h2 {
  margin-top: 42px;
  font: 600 20px var(--serif);
  color: var(--ink1);
}
.doc h3 { margin-top: 26px; font-size: 15.5px; }
.doc p, .doc li { margin-top: 13px; color: var(--ink2); font-size: 15px; line-height: 1.7; }
.doc ul { margin-top: 6px; padding-left: 20px; }
.doc li::marker { color: var(--ink4); }
.doc strong { color: var(--ink1); font-weight: 600; }

/* The one thing on a legal page worth pulling out of the flow: the sentence
   somebody actually came to read. */
.doc .headline-claim {
  margin-top: 26px;
  padding: 20px 22px;
  border-left: 2px solid var(--brass);
  background: rgba(192,143,78,.06);
  font: 500 16px/1.6 var(--sans);
  color: var(--ink1);
}

.doc table {
  margin-top: 18px;
  width: 100%;
  border-collapse: collapse;
  font-size: 13.5px;
}
.doc th, .doc td {
  text-align: left;
  padding: 10px 12px 10px 0;
  border-bottom: 1px solid var(--hair-soft);
  vertical-align: top;
}
.doc th {
  font: 700 9.5px var(--sans);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink3);
}
.table-wrap { overflow-x: auto; }

/* --- footer -------------------------------------------------------------- */

footer.site {
  margin-top: 70px;
  padding-top: 24px;
  border-top: 1px solid var(--hair);
  display: flex;
  justify-content: space-between;
  gap: 18px;
  flex-wrap: wrap;
  align-items: baseline;
}
footer.site .tagline {
  font: italic 500 14px var(--serif);
  color: var(--ink3);
}
footer.site nav { display: flex; gap: 18px; flex-wrap: wrap; }
footer.site nav a {
  font: 400 13px var(--sans);
  color: var(--ink3);
  text-decoration: none;
}
footer.site nav a:hover { color: var(--brass); }

.credits {
  margin-top: 16px;
  font: 400 12px/1.6 var(--sans);
  color: var(--ink4);
  max-width: var(--measure);
}

@media (prefers-reduced-motion: no-preference) {
  .fade { animation: rise .5s cubic-bezier(.2,.7,.3,1) both; }
  @keyframes rise {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: none; }
  }
}
