/* ============================================================
   THE RAFFLE — BASE
   Reset, typographic roles, layout primitives, utilities, motion.
   Tokens come exclusively from css/tokens.css (see /DESIGN.md).
   ============================================================ */

/* ============================================================
   RESPONSIVE CONTRACT (DESIGN.md → Layout / Responsive)
   Two axes, kept deliberately separate because they answer
   different questions:

   1. WIDTH — how much room a component has. One four-step
      ladder, used by every component block:

        ≤ 480px   compact phone   one column, 20px gutter
        ≤ 768px   phone           the main storefront breakpoint
        ≤ 900px   small tablet    2-up grids fold to 1-up
        ≤ 1024px  tablet          detail rails unstack; sidebar
                                  goes off-canvas

      CSS cannot read a custom property inside a media query, so
      the ladder is a convention, not a token. Anything that needs
      to answer to its own container rather than the viewport uses
      a container query instead (see .countdown-host, .rw-*).

   2. INPUT — how precisely the user can point. `pointer: coarse`
      raises every control to the 44px touch floor without
      thickening the compact desktop density DESIGN.md specifies.

      The floor is applied per component, never as a blanket
      element rule: several controls (the stepper's − / +, the
      widget's carousel dots, the dev bar's pills) are deliberately
      smaller than their hit area and grow by padding or a
      transparent pseudo-element rather than by box height. Each
      component block carries its own rule so it stays liftable;
      only the shared value lives here.
   ============================================================ */
:root { --touch-target: 44px; }

/* ── Reset ─────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* THE THREE DECLARATIONS THAT DECIDE WHETHER A PHONE READS THIS AS
   AN APP OR AS A WEBSITE, and none of them is a design decision:

   - The tap highlight is a translucent grey plate the browser paints
     over anything with a click handler. It is the loudest "this is a
     web page" signal there is, and it fights the press feedback
     DESIGN.md specifies (Motion Philosophy — every pressable control
     scales on :active). Removing it is only safe BECAUSE that press
     state exists on every control; a control with neither would give
     a thumb no answer at all.
   - Text inflation is Safari's own idea of what a paragraph should
     measure in landscape. The type scale is not a suggestion.

   THE ROOT KEEPS ITS PULL-TO-REFRESH. This is a shop that is read
   as much as it is used, and a page that refuses to bounce at the
   top reads as a page that has stopped responding. What actually
   had to be fixed is scroll CHAINING — a sheet, a drawer, a menu or
   a rail handing its overscroll to the page behind it — and that is
   `overscroll-behavior: contain` on each of those containers, in
   their own blocks, where a component stays liftable. */
html {
  scroll-behavior: smooth;
  -webkit-tap-highlight-color: transparent;
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  background: var(--surface-canvas);
  color: var(--color-ink);
  /* dvh, not vh: `vh` is the viewport with the browser chrome
     COLLAPSED, so a `100vh` floor on the page makes every phone
     scroll by the height of its own URL bar before there is any
     content to scroll. */
  min-height: 100dvh;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  padding-top: calc(var(--dev-bar-h) + var(--nav-h));
}

/* A tap should never wait 300ms to find out whether it was a double
   tap, and a long press on a control should not offer to select its
   label — a button's text is not content. Prose stays selectable;
   `user-select` is applied per control, never to the body. */
a, button, label, summary, select, [role="button"], [role="tab"] { touch-action: manipulation; }

a { color: var(--color-ink); text-decoration: none; }
/* The hover underline is a cursor's answer. On a touch device
   :hover latches after a tap, so an ungated rule leaves an underline
   sitting on whatever was last tapped — which is why the touch reply
   is the :active state each component declares instead. */
@media (hover: hover) and (pointer: fine) {
  a:hover { text-decoration: underline; }
}
/* WHICH LEAVES A LINK IN PROSE WITH NOTHING TO SAY IT IS ONE. In a
   run of authored text a link is not marked by its position or its
   shape, only by its colour — and colour alone is not a distinction
   a reader is required to be able to make. So in the two containers
   that hold authored prose the underline is permanent rather than a
   reward for pointing at it. Every link that is a CONTROL — a
   button, a nav row, a tile — says what it is by being one. */
.rich-text a, .legal-body a { text-decoration: underline; text-underline-offset: 3px; }

img { max-width: 100%; height: auto; display: block; }
/* The line above is a `display` on an element type, so it beats the
   browser's own rule for the `hidden` attribute and an <img hidden>
   would draw — as a broken-image mark, which is the one thing worse
   than the picture. The attribute means hidden everywhere. */
[hidden] { display: none !important; }

button { font: inherit; }

ul, ol { list-style: none; }

/* ── Typographic roles (DESIGN.md type scale) ──────────────────
   THE TRACKING COMES WITH THE SIZE, so it is set on the same line
   that sets the size rather than once over the whole family. A role
   that names a size takes that size's rung (tokens.css → Typography
   — tracking); `--ls-rg` stays as the family default for the couple
   of roles below that are a FACE rather than a size. */
h1, h2, h3, h4,
.text-display, .text-display-sm,
.text-heading-lg, .text-heading, .text-heading-md, .text-heading-sm, .text-subheading,
.font-display, .font-serif,
.hatton {
  letter-spacing: var(--ls-rg);
  line-height: 1;
}

h1, h2, h3, h4 { font-family: var(--font-display); color: var(--color-obsidian); }
h1 { font-size: var(--text-heading-lg); font-weight: var(--font-weight-bold); letter-spacing: var(--ls-heading-lg); }
h2 { font-size: var(--text-heading); font-weight: var(--font-weight-bold); letter-spacing: var(--ls-heading); }
h3 { font-size: var(--text-heading-sm); font-weight: var(--font-weight-semibold); letter-spacing: var(--ls-heading-sm); }
h4 { font-size: var(--text-subheading); font-weight: var(--font-weight-semibold); letter-spacing: var(--ls-subheading); }
h5 { font-size: var(--text-body-lg); font-family: var(--font-body); font-weight: var(--font-weight-semibold); line-height: var(--leading-body-lg); color: var(--color-ink); }

p { color: var(--color-ink); }

.text-display { font-family: var(--font-display); font-size: var(--text-display); font-weight: var(--weight-display); letter-spacing: var(--ls-display); }
.text-display-sm { font-family: var(--font-display); font-size: var(--text-display-sm); font-weight: var(--weight-display); letter-spacing: var(--ls-display); }
.text-heading-lg { font-family: var(--font-display); font-size: var(--text-heading-lg); font-weight: var(--font-weight-bold); letter-spacing: var(--ls-heading-lg); }
.text-heading { font-family: var(--font-display); font-size: var(--text-heading); font-weight: var(--font-weight-bold); letter-spacing: var(--ls-heading); }
.text-heading-md { font-family: var(--font-display); font-size: var(--text-heading-md); font-weight: var(--font-weight-semibold); letter-spacing: var(--ls-heading-md); }
.text-heading-sm { font-family: var(--font-display); font-size: var(--text-heading-sm); font-weight: var(--font-weight-semibold); letter-spacing: var(--ls-heading-sm); }
.text-subheading { font-family: var(--font-display); font-size: var(--text-subheading); font-weight: var(--font-weight-semibold); letter-spacing: var(--ls-subheading); }
.text-body-lg { font-size: var(--text-body-lg); line-height: var(--leading-body-lg); }
.text-body-md { font-size: var(--text-body-md); line-height: var(--leading-body-md); }
.text-body-sm { font-size: var(--text-body-sm); line-height: var(--leading-body-sm); }
.text-body-xs { font-size: var(--text-body-xs); line-height: var(--leading-body-xs); }
.text-caption { font-size: var(--text-caption); line-height: var(--leading-caption); font-weight: var(--weight-caption); }

/* ── Rich text ──────────────────────────────────────────────────
   Host-authored prose (a draw's description, written in the console's
   rich-text field). The author picks blocks, never sizes, so this maps
   the handful of tags that field can emit back onto the type scale —
   the only place raw h3/h4/ul/ol/strong are styled by element rather
   than by class. Nothing outside that set is emitted, so nothing
   outside it is styled. */
.rich-text > :first-child { margin-top: 0; }
.rich-text > :last-child { margin-bottom: 0; }
.rich-text p { margin: 0 0 var(--spacing-16); }
.rich-text h3 {
  font-family: var(--font-display); letter-spacing: var(--ls-rg);
  font-size: var(--text-subheading); font-weight: var(--font-weight-semibold);
  margin: var(--spacing-28) 0 var(--spacing-8);
}
.rich-text h4 {
  font-family: var(--font-display); letter-spacing: var(--ls-rg);
  font-size: var(--text-body-lg); font-weight: var(--font-weight-semibold);
  margin: var(--spacing-20) 0 var(--spacing-8);
}
.rich-text ul, .rich-text ol { margin: 0 0 var(--spacing-16); padding-left: var(--spacing-20); }
/* The global reset strips markers; authored lists are the one place
   they carry meaning, so they come back here. */
.rich-text ul { list-style: disc; }
.rich-text ol { list-style: decimal; }
.rich-text li { margin-bottom: var(--spacing-4); }
.rich-text strong, .rich-text b { font-weight: var(--font-weight-semibold); }

/* ── Icons (Tabler outline set via js/icons.js) ────────────── */
/* DESIGN.md: minimal, monochrome, single-stroke, ~20px, never decorative */
.icon { width: 20px; height: 20px; flex-shrink: 0; display: inline-block; vertical-align: -0.25em; }
.icon-sm { width: 16px; height: 16px; vertical-align: -0.185em; }
.icon-lg { width: 24px; height: 24px; }
.icon-xl { width: 32px; height: 32px; }
i[data-icon] { display: inline-flex; align-items: center; justify-content: center; font-style: normal; line-height: 1; flex-shrink: 0; }

/* Micro-label: the only place uppercase tracking is allowed (≤12px) */
.micro-label {
  font-size: var(--text-body-xs);
  font-weight: var(--font-weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-steel);
}

/* ── Layout primitives ─────────────────────────────────────── */
.container { max-width: var(--page-max-width); margin: 0 auto; padding: 0 var(--spacing-40); }
@media (max-width: 768px) { .container { padding: 0 var(--spacing-20); } }

main { padding-bottom: var(--spacing-80); }

/* Every block owns HALF the gap on each of its own edges, so the space
   between any two sections is one section's bottom pad plus the next
   one's top pad — 48 + 48, the same 96px rhythm, drawn symmetrically.
   It has to be symmetric because a section can be given a ground of
   its own (components.css → SECTION GROUND): carrying the whole gap on
   the top edge puts 96px of colour above a dark band's content and
   nothing below it, which is the one place the asymmetry is visible
   rather than merely notional. The other blocks in the flow keep their
   half of the same bargain — a full-bleed page head (.showcase,
   .page-banner, .legal-head) pays 48px below itself, and the footer
   pays 48px above. */
.section { padding: var(--spacing-48) 0; }
.section-sm { padding: var(--spacing-20) 0; }
/* Mobile rhythm steps down (DESIGN.md Layout): 32 + 32 = 64px between
   sections, holding the 1.5:1 desktop:mobile ratio. */
@media (max-width: 768px) { .section { padding: var(--spacing-32) 0; } }

.page-header { padding: var(--spacing-40) 0 var(--spacing-24); }
.page-title { font-size: var(--text-heading); line-height: var(--leading-heading); margin-bottom: var(--spacing-8); }
.page-subtitle { color: var(--color-steel); font-size: var(--text-body-md); }

.section-header { margin-bottom: var(--spacing-32); }
.section-title { font-size: var(--text-heading); line-height: var(--leading-heading); letter-spacing: var(--ls-heading); margin-bottom: var(--spacing-8); }
.section-subtitle { color: var(--color-steel); }

.divider { border: none; border-top: 1px solid var(--color-fog); margin: var(--spacing-24) 0; }

.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--spacing-24); }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-24); }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--spacing-24); }

/* A grid item's automatic minimum size is its CONTENT, not zero, so a
   `1fr` track silently grows past its share whenever something inside
   refuses to wrap — a long word, a nowrap row, a fixed-width control.
   On a phone that is the single most common cause of a page scrolling
   sideways. min-width: 0 makes `1fr` mean what it says. Same guard
   .layout-split carries below; component grids that set their own
   tracks repeat it in their own block. */
.grid-2 > *, .grid-3 > *, .grid-4 > * { min-width: 0; }

/* Stay two-up on a phone instead of collapsing to one column. For
   BROWSING grids — a long list of draw tiles — where one tile per row
   turns a page of choices into a page of scrolling, and the tile is a
   picture that still reads at half width. Not for grids of text cards,
   forms or stats, which need the full measure. The tiles inside answer
   to their own width via container queries, so they tighten to suit
   without this modifier having to know anything about them.
   Pairs with .rail-mobile (components.css → PRIZE TILE), the other
   mobile treatment for a draw-card grid: use the rail for a short
   curated strip, this for a full listing. */
@media (max-width: 768px) {
  .grid-mobile-2 {
    grid-template-columns: repeat(2, 1fr) !important;
    gap: var(--spacing-12);
  }
}

@media (max-width: 1024px) { .grid-4 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px)  { .grid-4 { grid-template-columns: 1fr; } }

.flex { display: flex; align-items: center; gap: var(--spacing-16); }
.flex-between { display: flex; align-items: center; justify-content: space-between; }
.flex-center { display: flex; align-items: center; justify-content: center; }
.flex-col { display: flex; flex-direction: column; gap: var(--spacing-16); }
.flex-wrap { flex-wrap: wrap; }
/* Shrink-to-fit row (unlike .flex, which stretches full-width) — for
   markers that use margin:auto to self-centre when
   there's no leftover space to distribute, e.g. beside a label. */
.inline-flex { display: inline-flex; align-items: center; gap: var(--spacing-12); }

/* Reading-width cap for lead/body copy (DESIGN.md: 65-75ch) */
.measure { max-width: 62ch; }

.resp-grid-2col { display: grid; }

/* ── Utilities ─────────────────────────────────────────────── */
.text-center { text-align: center; }
.text-right { text-align: right; }
.text-muted { color: var(--color-steel); }
.text-accent { color: var(--color-ink); }
.text-success { color: var(--success); }
.text-danger { color: var(--danger); }
.text-warning { color: var(--warning); }
.text-sm { font-size: var(--text-body-sm); }
.text-xs { font-size: var(--text-body-xs); }
.text-lg { font-size: var(--text-body-lg); }
.text-xl { font-size: var(--text-heading-sm); }
/* Machine references — draw refs, certification and entry numbers,
   order and tracking numbers, measurements. Inter at whatever size the
   context already sets; tabular figures keep a stacked column of them
   aligned, and the 500 weight is what separates a reference from the
   prose around it. No tracking — DESIGN.md allows uppercase tracking
   only on micro-labels at 12px and below. Declared above the weight
   utilities so `.font-semibold` / `.font-bold` still win. */
.ref-text {
  font-family: var(--font-body);
  font-weight: var(--font-weight-medium);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}
/* The one monospace face in the platform, and it is not interface
   text — it is literal code the reader may copy: embed snippets,
   attribute names, cookie names, hex values. A reference number is
   `.ref-text`; a price is a price. Nothing else takes this. */
.code { font-family: var(--font-mono); font-size: 0.92em; letter-spacing: 0; }
.font-bold { font-weight: var(--font-weight-bold); }
.font-semibold { font-weight: var(--font-weight-semibold); }
.font-regular { font-weight: var(--font-weight-regular); }
.font-light { font-weight: var(--font-weight-light); }
.font-serif { font-family: var(--font-display); }
.font-display { font-family: var(--font-display); }
/* Switzer Bold Italic accent — wrap highlighted words/phrases in headlines only */
.hatton { font-family: var(--font-hatton); font-weight: 700; font-style: italic; }

.mt-1 { margin-top: var(--spacing-4); } .mt-2 { margin-top: var(--spacing-8); } .mt-3 { margin-top: var(--spacing-12); }
.mt-4 { margin-top: var(--spacing-16); } .mt-6 { margin-top: var(--spacing-24); } .mt-8 { margin-top: var(--spacing-32); }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: var(--spacing-4); } .mb-2 { margin-bottom: var(--spacing-8); } .mb-3 { margin-bottom: var(--spacing-12); }
.mb-4 { margin-bottom: var(--spacing-16); } .mb-6 { margin-bottom: var(--spacing-24); } .mb-8 { margin-bottom: var(--spacing-32); }
.gap-1 { gap: var(--spacing-4); } .gap-2 { gap: var(--spacing-8); } .gap-3 { gap: var(--spacing-12); } .gap-4 { gap: var(--spacing-16); } .gap-6 { gap: var(--spacing-24); }

.w-full { width: 100%; }
.flex-1 { flex: 1; }
.flex-2 { flex: 2; }
.ml-auto { margin-left: auto; }
.mx-auto { margin-left: auto; margin-right: auto; }
.thumb-sm { width: 56px; height: 44px; object-fit: cover; border-radius: var(--radius-xl); flex-shrink: 0; }
.thumb-md { width: 80px; height: 60px; object-fit: cover; border-radius: var(--radius-xl); flex-shrink: 0; }
.hidden { display: none !important; }
.visible { display: block !important; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); clip-path: inset(50%); white-space: nowrap; border: 0; }

.rounded { border-radius: var(--radius-xl); }
.rounded-lg { border-radius: var(--radius-cards-compact); }
.rounded-xl { border-radius: var(--radius-cards); }
.rounded-full { border-radius: var(--radius-pill); }

.shadow { box-shadow: var(--shadow-flat-hairline); }
.shadow-lg { box-shadow: var(--shadow-flat-hairline); }

.bg-card { background: var(--surface-card-white); }
.bg-muted { background: var(--surface-card-muted); }
.p-0 { padding: 0; }
.p-3 { padding: var(--spacing-12); } .p-4 { padding: var(--spacing-16); } .p-5 { padding: var(--spacing-20); } .p-6 { padding: var(--spacing-24); } .p-7 { padding: var(--spacing-28); }
.py-4 { padding-top: var(--spacing-16); padding-bottom: var(--spacing-16); }
.py-6 { padding-top: var(--spacing-24); padding-bottom: var(--spacing-24); }
.py-8 { padding-top: var(--spacing-32); padding-bottom: var(--spacing-32); }
.scroll-box { max-height: 340px; overflow-y: auto; overscroll-behavior: contain; }

/* Split layout: content + sticky rail (detail pages) */
.layout-split { display: grid; grid-template-columns: 2fr 1fr; gap: var(--spacing-32); align-items: start; }
.layout-split > * { min-width: 0; }  /* tracks never grow past their fr share */
.sticky-rail { position: sticky; top: var(--sticky-top); }
/* Summary-led variant: the rail column wraps a lead summary card above
   the sticky rail (a flex column; its 16px gap matches the purchase
   card → host card spacing so the rail reads as one stack). On mobile
   the wrapper dissolves (display: contents) and the summary orders
   itself above the content — prize name first, then gallery/tabs, then
   the purchase rail. */
.layout-split > .split-rail { display: flex; flex-direction: column; gap: var(--spacing-16); align-self: stretch; }
.split-rail > * { min-width: 0; }
@media (max-width: 1024px) {
  .layout-split { grid-template-columns: 1fr; }
  .sticky-rail { position: static; }
  .layout-split > .split-rail { display: contents; }
  .split-rail > .split-summary { order: -1; }
}

.border { border: 1px solid var(--color-fog); }
.border-t { border-top: 1px solid var(--color-fog); }
.border-b { border-bottom: 1px solid var(--color-fog); }

/* ── Motion primitives (DESIGN.md "Motion Philosophy") ─────── */
/* Entrance animation — the ONLY place the overshoot spring is used */
.fade-up {
  animation: fadeUp 0.6s var(--ease-spring) both;
}
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(16px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Scroll-reveal group — children cascade in with a 60ms stagger
   when the group scrolls into view (ui.js observes .reveal-group
   and flips .is-in). JS adds .reveal-ready, so nothing is hidden
   without JS. Keyframe animation with backwards fill only: after
   it finishes the element returns to its natural styles, so the
   reveal never fights a component's own hover transitions. */
.reveal-group.reveal-ready > * { opacity: 0; }
.reveal-group.is-in > * { opacity: 1; animation: revealUp 0.5s var(--ease-deck-out) backwards; }
.reveal-group.is-in > :nth-child(2) { animation-delay: 60ms; }
.reveal-group.is-in > :nth-child(3) { animation-delay: 120ms; }
.reveal-group.is-in > :nth-child(4) { animation-delay: 180ms; }
.reveal-group.is-in > :nth-child(n+5) { animation-delay: 240ms; }
@keyframes revealUp {
  from { opacity: 0; transform: translateY(14px); }
}

/* REDUCED MOTION IS NOT NO FEEDBACK. The setting asks for nothing
   that MOVES — no travel, no parallax, no spring, no overshoot,
   because those are what trigger a vestibular response. It does not
   ask for a page where pressing something produces no answer and a
   panel appears out of nowhere: a cross-fade is not motion in the
   sense the setting means, and it is often the only thing telling
   the reader that what they did worked.

   This used to be `animation: none` and a transition duration of
   nothing, on everything — which also took away every colour and
   opacity change in the app, so the shop went from fluid to inert
   rather than from fluid to calm. What it says instead is: keep the
   animations off, and keep exactly the transitions that carry no
   travel. Naming the properties is what does it — `transform` is
   not on the list, so every scale, lift, slide and pan is dropped
   wherever it was declared, without a single component having to
   know the setting exists. What survives is a 200ms fade.

   The behaviours that cannot be expressed as a property — a
   marquee looping, a hero advancing itself, a clip autoplaying —
   are turned off in JS, where each of them is already gated. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-property: opacity, color, background-color, border-color, fill, stroke, box-shadow !important;
    transition-duration: 200ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
}

