/* ---------------------------------------------------------------------------
   Popsy Party — design system

   The governing idea: the chrome is a gallery wall, the collections bring the
   colour. Ten themes are coming and each has its own palette — terracotta and
   ochre, honey gold and sage, sea glass and teal, mustard and avocado. A
   storefront that wraps all ten in brand-pink candy stripes fights every one
   of them, so the surfaces here are paper and hairline rules, and each pack
   tints its own card and page through `--accent` (packs.accent_hex).

   Whimsy lives in a few deliberate moments rather than in the wallpaper: the
   arch, the one line-drawn balloon dog, a slow reveal, the script accent on a
   single phrase. Everything else is quiet on purpose.

   Type is Poppins throughout, per the brand board. Handelson Three is
   Canva-licensed and cannot be a webfont — it survives only as the exported
   logo artwork, with Dancing Script standing in as a rare accent.
--------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   Fonts, self-hosted.

   These used to come from fonts.googleapis.com. That stylesheet was a
   render-blocking request to a third-party origin, and Lighthouse put it at
   844 ms of the home page's 2.7 s first paint — on a page whose LCP element is
   a paragraph of text, so nothing could paint until it resolved. Served from
   this origin the request is same-connection and the two preconnects and the
   fonts.gstatic.com hop are gone, which also let the Content-Security-Policy
   in src/lib/http.ts close style-src and font-src down to 'self'.

   latin and latin-ext only — the site's copy is English with typographic
   punctuation, and unicode-range means a browser downloads latin-ext only if
   it actually meets a character in it. 124 KB on disk, ~48 KB on a typical
   page. Regenerate by re-fetching the same css2 URL, keeping the latin and
   latin-ext blocks, and rewriting each src to /assets/fonts/.

   Poppins keeps font-display: swap — fallback text first, real face when it
   arrives — because 'Poppins Fallback' below makes that swap metrically
   invisible.

   Dancing Script is font-display: OPTIONAL, and that is the fix for a real
   bug. It appears once a page as the .script accent standing in for the
   logo's Handelson Three, which is Canva-licensed and can never be a
   webfont — and nothing is metrically close to it. On /faq the h1 is
   "Questions, answered": 43 px tall in Dancing Script and 83 px in the
   cursive fallback at 412 px wide, because the accent word tips the heading
   from one line to two. Swapping that in after paint moved the entire page
   down 40 px, which Lighthouse scored as CLS 0.182 against a 0.1 budget.
   'optional' means the browser either has the face by first paint or uses
   the fallback for the whole load and never swaps — so the shift cannot
   happen. It is preloaded in views/layout.ts so that in practice it is
   there.
--------------------------------------------------------------------------- */

@font-face {
  font-family: 'Dancing Script';
  font-style: normal;
  font-weight: 700;
  font-display: optional;
  src: url(/assets/fonts/dancing-script-700-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Dancing Script';
  font-style: normal;
  font-weight: 700;
  font-display: optional;
  src: url(/assets/fonts/dancing-script-700-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/assets/fonts/poppins-300-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url(/assets/fonts/poppins-300-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/poppins-400-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/poppins-400-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/poppins-500-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/poppins-500-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/assets/fonts/poppins-600-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/assets/fonts/poppins-600-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/assets/fonts/poppins-700-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/assets/fonts/poppins-700-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* The metric-matched fallback, and why it exists.
 *
 * `font-display: swap` shows fallback text first and swaps Poppins in when it
 * arrives. On short lines that is invisible; on the FAQ, 1400 characters of
 * body copy, the swap changed the line count and moved everything below it —
 * Lighthouse measured CLS 0.182 there against a 0.1 budget, on a page that was
 * otherwise 100.
 *
 * Arial is 8% wider than Poppins across a representative sample (measured:
 * 12168 vs 11201 units for the same string at 100px, a ratio of 92.05%), so
 * scaling it down by that much makes the fallback wrap where Poppins wraps.
 * The ascent, descent and line-gap overrides are Poppins' own hhea values
 * (1050 / -350 / 100 per 1000 upm), so the line box matches too.
 *
 * It sits ahead of the platform stack deliberately: -apple-system and Segoe UI
 * have different metrics from each other, and one size-adjust cannot match
 * both. Arial, Helvetica, Liberation Sans and Arimo are metrically identical
 * and present essentially everywhere, so this gives every platform the same
 * adjusted fallback. The platform stack stays behind it as a last resort. */
@font-face {
  font-family: 'Poppins Fallback';
  src: local('Arial'), local('Helvetica'), local('Liberation Sans'), local('Arimo');
  /* Every weight, or the browser skips this face for the 300/500/600/700 the
     site actually uses and falls through to the unadjusted platform stack —
     which is most of the headings, and most of the shift. */
  font-weight: 100 900;
  font-style: normal;
  size-adjust: 92.05%;
  ascent-override: 105%;
  descent-override: 35%;
  line-gap-override: 10%;
}

:root {
  /* the five board swatches */
  --raspberry: #af3c5d;
  --olive: #949d2e;
  --blush: #ffedfa;
  --cream: #fffdf4;
  --white: #fffffa;

  /* paper and ink — the surfaces the catalogue sits on */
  --paper: #fdfaf2;
  --paper-warm: #f9f2e8;
  --ink: #2c2028;
  /* Secondary text. Not a board swatch — a UI grey, and it was #7d6f76, which
     cleared 4.5:1 on --paper (4.57) but not on the two warmer surfaces it also
     sits on: --paper-warm 4.29 and --blush 4.25. Darkened until every surface
     in this file clears the bar; the worst is now --blush at 4.78. */
  --muted: #75676e;
  --hairline: #eae0dd;

  /* Per-pack accent; overridden inline from packs.accent_hex on the card and
     on the product page's wrapper — see accentStyle() in views/storefront.ts.

     Do NOT derive a second variable from this one here. A custom property
     declared on :root resolves its var() against :root, so
     `--accent-tint: color-mix(..., var(--accent), ...)` written at this level
     is frozen at the raspberry default and never sees a pack's colour. Write
     the color-mix() at the point of use instead, where var(--accent) picks up
     the inline value. (--accent-tint was exactly this mistake; it was declared
     here, never used anywhere, and would have rendered the wrong colour if it
     had been.)

     ACCENT AS TEXT OR AS AN EDGE. Five of the ten pack accents fail WCAG AA as
     small text on --paper — Sweet as Honey 2.89:1, Spa Day 2.91, Groovy One
     3.85, Main Stage 4.07, Wildflower Garden 4.35 against the 4.5 bar — and
     three also fail the 3:1 a focus ring or a selected border owes. Mixing 30%
     ink in keeps every hue recognisable and lifts the worst to 4.60:1. So:
     bare var(--accent) for washes and decoration, and
     `color-mix(in srgb, var(--accent) 70%, var(--ink))` for anything a person
     has to read or see the edge of. */
  --accent: var(--raspberry);

  --raspberry-deep: #8d2f4a;

  --lift: 0 1px 2px rgba(44, 32, 40, 0.04), 0 12px 28px -12px rgba(44, 32, 40, 0.16);
  --lift-lg: 0 2px 4px rgba(44, 32, 40, 0.04), 0 30px 60px -24px rgba(44, 32, 40, 0.24);

  --radius: 14px;
  --radius-lg: 22px;
  /* the arch — the one shape that carries the whimsy */
  --arch: 999px 999px 22px 22px;

  --page: 1180px;
  --gutter: 26px;

  --ui: 'Poppins', 'Poppins Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
  --script: 'Dancing Script', 'Snell Roundhand', cursive;

  /* The board's candy stripes and checkerboard, kept for detail work only:
     a backing panel behind an arch, a trim along the footer, a texture on the
     announcement bar. Never a full-bleed background — at this contrast they
     read as paper stock, and they stay off anything carrying body text. */
  --stripes: repeating-linear-gradient(90deg, var(--blush) 0 15px, var(--cream) 15px 30px);
  --checks:
    conic-gradient(from 90deg at 50% 50%, var(--blush) 25%, var(--cream) 0 50%, var(--blush) 0 75%, var(--cream) 0);
}

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

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

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--ui);
  font-size: 16.5px;
  line-height: 1.7;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

main {
  flex: 1 0 auto;
}

img {
  max-width: 100%;
  display: block;
}

a {
  color: var(--raspberry);
  text-underline-offset: 3px;
}

h1,
h2,
h3 {
  margin: 0 0 0.45em;
  font-weight: 500;
  line-height: 1.12;
  letter-spacing: -0.03em;
}

h1 {
  font-size: clamp(2.3rem, 4.6vw, 3.5rem);
}
h2 {
  font-size: clamp(1.75rem, 3vw, 2.4rem);
}
h3 {
  font-size: 1.1rem;
  font-weight: 600;
  letter-spacing: -0.015em;
}

h4 {
  margin: 0 0 0.7em;
  font-weight: 600;
}

p {
  margin: 0 0 1em;
}

.wrap {
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 var(--gutter);
  position: relative;
}

.narrow {
  max-width: 760px;
}

section {
  padding: 96px 0;
  position: relative;
}

/* Small-caps label. No pill, no background — it should read as a whisper. */
.eyebrow {
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 18px;
}

.lede {
  font-size: 1.1rem;
  color: var(--muted);
  max-width: 58ch;
}

/* The accent script: one phrase, once a page. Standing in for the logo's
   Handelson Three, which cannot ship as a webfont. */
.script {
  font-family: var(--script);
  font-weight: 700;
  font-size: 1.22em;
  line-height: 0.9;
  letter-spacing: 0;
  padding-right: 0.06em;
  white-space: nowrap;
  color: var(--raspberry);
}

/* --- buttons ------------------------------------------------------------ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 14px 30px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--raspberry);
  color: var(--white);
  font: 500 0.94rem/1 var(--ui);
  letter-spacing: 0.01em;
  text-decoration: none;
  cursor: pointer;
  transition:
    background 0.2s ease,
    transform 0.2s ease,
    box-shadow 0.2s ease;
}
.btn:hover {
  background: var(--raspberry-deep);
  transform: translateY(-2px);
  box-shadow: var(--lift);
}
.btn--ghost {
  background: transparent;
  color: var(--ink);
  border-color: var(--hairline);
}
.btn--ghost:hover {
  background: var(--white);
  color: var(--ink);
  border-color: var(--ink);
}
.btn--block {
  width: 100%;
}
.btn--soon,
.btn[disabled] {
  background: transparent;
  color: var(--muted);
  border-color: var(--hairline);
  cursor: default;
}
.btn--soon:hover,
.btn[disabled]:hover {
  background: transparent;
  transform: none;
  box-shadow: none;
}

/* The skip link. Off-screen rather than display:none, so it stays in the tab
   order, and it lands on <main id="main">. It has to clear the sticky header,
   which is z-index 40. */
.skip-link {
  position: absolute;
  left: 12px;
  top: -60px;
  z-index: 60;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 10px 18px;
  border-radius: 0 0 var(--radius) var(--radius);
  background: var(--ink);
  color: var(--paper);
  font: 500 0.9rem/1 var(--ui);
  text-decoration: none;
  transition: top 0.15s ease;
}
.skip-link:focus-visible,
.skip-link:focus {
  top: 0;
  outline: 2px solid var(--paper);
  outline-offset: -6px;
}

/* --- header ------------------------------------------------------------- */

.announce {
  background: var(--ink);
  background-image: conic-gradient(
    from 90deg at 50% 50%,
    rgba(255, 255, 255, 0.028) 25%,
    transparent 0 50%,
    rgba(255, 255, 255, 0.028) 0 75%,
    transparent 0
  );
  background-size: 34px 34px;
  color: var(--paper);
  text-align: center;
  font-size: 0.78rem;
  letter-spacing: 0.03em;
  padding: 9px var(--gutter);
}
.announce a {
  color: var(--paper);
}

.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  background: rgba(253, 250, 242, 0.86);
  backdrop-filter: saturate(1.3) blur(12px);
  border-bottom: 1px solid var(--hairline);
}
.site-header .bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  min-height: 78px;
}
.brand img {
  height: 38px;
  width: auto;
}
.nav {
  display: flex;
  align-items: center;
  gap: 34px;
}
.nav a {
  color: var(--ink);
  text-decoration: none;
  font-size: 0.92rem;
  position: relative;
  padding: 4px 0;
}
.nav a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 100%;
  bottom: 0;
  height: 1px;
  background: var(--raspberry);
  transition: right 0.25s ease;
}
.nav a:hover::after,
.nav a[aria-current='page']::after {
  right: 0;
}
.nav a[aria-current='page'] {
  color: var(--raspberry);
}
.nav-toggle {
  display: none;
  /* 44x44 is the smallest thing a thumb reliably hits, and this is the only
     way to the nav on a phone. The 26px icon plus 8px padding was 42 wide. */
  min-width: 44px;
  min-height: 44px;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  padding: 8px;
  cursor: pointer;
  color: var(--ink);
}

@media (max-width: 860px) {
  .nav {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    padding: 12px var(--gutter) 20px;
    background: var(--paper);
    border-bottom: 1px solid var(--hairline);
  }
  .nav.open {
    display: flex;
  }
  .nav a {
    padding: 11px 0;
    font-size: 1rem;
  }
  .nav-toggle {
    display: block;
  }
  .site-header .bar {
    position: relative;
  }
}

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

.hero {
  padding: 76px 0 88px;
  background: var(--paper);
}
.hero .grid {
  display: grid;
  grid-template-columns: 1.02fr 0.98fr;
  gap: 72px;
  align-items: center;
}
.hero h1 {
  margin-bottom: 22px;
}
.hero .cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin-top: 32px;
}
.hero-marks {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 28px;
  margin-top: 40px;
  padding-top: 24px;
  border-top: 1px solid var(--hairline);
  font-size: 0.86rem;
  color: var(--muted);
}
.hero-dog {
  height: 74px;
  width: auto;
  margin-bottom: 26px;
  opacity: 0.9;
}

/* An arch, on a striped arch shifted behind it. One detail, doing the work
   the wallpaper used to do badly. */
.arch {
  position: relative;
}
.arch::before {
  content: '';
  position: absolute;
  inset: -18px;
  border-radius: var(--arch);
  background: var(--stripes);
}
.arch > * {
  position: relative;
}
/* the mat sits 18px proud of the frame — whatever follows has to clear it */
.arch + * {
  margin-top: 34px;
}
/* on a product page the backing picks up that pack's own colour */
.arch--accent::before {
  background: repeating-linear-gradient(
    90deg,
    color-mix(in srgb, var(--accent) 16%, var(--paper)) 0 15px,
    color-mix(in srgb, var(--accent) 6%, var(--paper)) 15px 30px
  );
}
@media (max-width: 600px) {
  .arch::before {
    inset: -12px;
  }
}

/* the hero photograph */
.hero-figure {
  position: relative;
}
.hero-figure img {
  width: 100%;
  /* capped: the arch is the shape, not the reason the hero is 900px tall */
  aspect-ratio: 5 / 5.4;
  max-height: 540px;
  object-fit: cover;
  border-radius: var(--arch);
  background: var(--paper-warm);
}
.hero-figure .ph-legend {
  margin-top: 16px;
}

@media (max-width: 900px) {
  .hero {
    padding: 60px 0 72px;
  }
  .hero .grid {
    grid-template-columns: 1fr;
    gap: 44px;
  }
  .hero-figure img {
    aspect-ratio: 4 / 3.4;
  }
}

/* --- section headings --------------------------------------------------- */

.section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 40px;
  margin-bottom: 46px;
}
.section-head p {
  margin: 0;
}
.section-head h2 {
  margin-bottom: 0;
}

/* --- collection cards --------------------------------------------------- */

.grid-4 {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(236px, 1fr));
  gap: 44px 30px;
}

.card {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: inherit;
}
.card .thumb {
  position: relative;
  aspect-ratio: 4 / 5;
  border-radius: var(--arch);
  overflow: hidden;
  background: color-mix(in srgb, var(--accent) 13%, var(--paper));
  transition: transform 0.4s cubic-bezier(0.2, 0.7, 0.3, 1);
}
.card .thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.3, 1);
}
.card:hover .thumb {
  transform: translateY(-6px);
}
.card:hover .thumb img {
  transform: scale(1.03);
}
.card .body {
  padding: 20px 4px 0;
}
.card .title {
  display: block;
  font-size: 1.08rem;
  font-weight: 500;
  letter-spacing: -0.02em;
}
.card .sub {
  color: var(--muted);
  font-size: 0.88rem;
  margin: 2px 0 0;
}
.card .price-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--hairline);
  font-size: 0.88rem;
}
.price {
  font-weight: 500;
}
.price-was {
  color: var(--muted);
  text-decoration: line-through;
  font-size: 0.85rem;
}
.card .meta-right {
  margin-left: auto;
  color: var(--muted);
}

/* a quiet status label, not a rotated sticker */
.badge {
  position: absolute;
  z-index: 1;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(253, 250, 242, 0.92);
  backdrop-filter: blur(4px);
  color: var(--ink);
  border-radius: 999px;
  padding: 5px 14px;
  font-size: 0.63rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* --- occasion tiles ----------------------------------------------------- */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 30px;
}
.tile {
  text-decoration: none;
  color: inherit;
}
.tile__frame {
  position: relative;
  aspect-ratio: 5 / 4;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--paper-warm);
  transition: transform 0.4s cubic-bezier(0.2, 0.7, 0.3, 1);
}
.tile:hover .tile__frame {
  transform: translateY(-6px);
}
.tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.tile__label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-top: 16px;
  font-size: 1.02rem;
  font-weight: 500;
}
.tile__label span:last-child {
  color: var(--muted);
  font-size: 0.85rem;
  transition: transform 0.25s ease;
}
.tile:hover .tile__label span:last-child {
  transform: translateX(4px);
}

/* --- steps -------------------------------------------------------------- */

.steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 52px;
  counter-reset: step;
}
.step {
  counter-increment: step;
  padding-top: 26px;
  border-top: 1px solid var(--hairline);
}
.step::before {
  content: counter(step, decimal-leading-zero);
  display: block;
  font-size: 0.76rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  color: var(--raspberry);
  margin-bottom: 14px;
}
.step h3 {
  margin-bottom: 8px;
}
.step p {
  color: var(--muted);
  margin: 0;
  font-size: 0.95rem;
}

/* --- soft bands --------------------------------------------------------- */

.band {
  background: var(--paper-warm);
}
.band-blush {
  background: var(--blush);
}

/* --- what's inside ------------------------------------------------------ */

.included {
  columns: 2;
  column-gap: 56px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.included li {
  break-inside: avoid;
  padding: 11px 0;
  border-bottom: 1px solid var(--hairline);
  font-size: 0.94rem;
  display: flex;
  gap: 12px;
  align-items: baseline;
}
.included li::before {
  content: '';
  flex: 0 0 auto;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--olive);
  transform: translateY(-2px);
}
@media (max-width: 640px) {
  .included {
    columns: 1;
  }
}

/* --- product page ------------------------------------------------------- */

.breadcrumb {
  padding-top: 26px;
  font-size: 0.83rem;
  color: var(--muted);
}
.breadcrumb a {
  color: var(--muted);
  text-decoration: none;
}
.breadcrumb a:hover {
  color: var(--raspberry);
}

.product {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 72px;
  align-items: start;
  padding: 44px 0 24px;
}
@media (max-width: 900px) {
  .product {
    grid-template-columns: 1fr;
    gap: 40px;
  }
}
.gallery-main {
  border-radius: var(--arch);
  overflow: hidden;
  background: color-mix(in srgb, var(--accent) 13%, var(--paper));
  aspect-ratio: 4 / 5;
  max-height: 620px;
}
.gallery-main img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.gallery-thumbs {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 14px;
  margin-top: 16px;
}
.gallery-thumbs button {
  padding: 0;
  border: 1px solid transparent;
  border-radius: 12px;
  overflow: hidden;
  background: color-mix(in srgb, var(--accent) 13%, var(--paper));
  cursor: pointer;
  aspect-ratio: 1;
  transition:
    border-color 0.2s ease,
    transform 0.25s ease;
}
.gallery-thumbs button:hover {
  transform: translateY(-3px);
}
.gallery-thumbs button[aria-selected='true'] {
  /* This border is the only thing marking which image is showing, so it is a
     UI component boundary and owes 3:1, not decoration. */
  border-color: color-mix(in srgb, var(--accent) 70%, var(--ink));
}
.gallery-thumbs img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.buybox .eyebrow {
  color: color-mix(in srgb, var(--accent) 70%, var(--ink));
}
.buybox h1 {
  font-size: clamp(2rem, 3.4vw, 2.7rem);
}
.buybox .price-row {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin: 20px 0 26px;
}
.buybox .price {
  font-size: 1.6rem;
  font-weight: 500;
}
.buybox .meta {
  margin: 30px 0 0;
  padding: 0;
  list-style: none;
  font-size: 0.92rem;
  color: var(--muted);
}
.buybox .meta li {
  padding: 13px 0;
  border-top: 1px solid var(--hairline);
}
.buybox .meta li:last-child {
  border-bottom: 1px solid var(--hairline);
}
.note {
  background: color-mix(in srgb, var(--accent) 13%, var(--paper));
  border-radius: var(--radius);
  padding: 16px 18px;
  font-size: 0.88rem;
  margin: 18px 0 0;
}
/* Checkout bounced the buyer back to the pack. Raspberry rather than the
   pack's own accent, so it reads as a message and not as decoration. */
.note--warn {
  background: color-mix(in srgb, var(--raspberry) 10%, var(--paper));
  border-left: 3px solid var(--raspberry);
  margin: 0 0 18px;
}

/* The buy button is wrapped in a real form; .btn--block already fills it. */
.buy-form {
  margin: 0;
}

details.faq {
  border-bottom: 1px solid var(--hairline);
  padding: 18px 0;
}
details.faq:first-child {
  border-top: 1px solid var(--hairline);
}
details.faq summary {
  cursor: pointer;
  font-weight: 500;
  font-size: 0.98rem;
  list-style: none;
  display: flex;
  justify-content: space-between;
  gap: 20px;
}
details.faq summary::-webkit-details-marker {
  display: none;
}
details.faq summary::after {
  content: '+';
  color: var(--muted);
  font-size: 1.2rem;
  line-height: 1;
  transition: transform 0.2s ease;
}
details.faq[open] summary::after {
  content: '–';
}
details.faq p {
  margin: 12px 0 0;
  color: var(--muted);
  font-size: 0.94rem;
  max-width: 62ch;
}

.pack-detail {
  display: grid;
  grid-template-columns: 1.08fr 1fr;
  gap: 72px;
}
@media (max-width: 860px) {
  .pack-detail {
    grid-template-columns: 1fr;
    gap: 40px;
  }
}

/* --- page headers ------------------------------------------------------- */

.page-head {
  padding: 76px 0 64px;
  border-bottom: 1px solid var(--hairline);
}
.page-head .wrap {
  display: flex;
  align-items: flex-end;
  gap: 48px;
  justify-content: space-between;
}
.page-head__text {
  max-width: 46rem;
}
.page-head h1 {
  margin-bottom: 16px;
}
.page-head .lede {
  margin: 0;
}
.page-head__art {
  flex: 0 0 auto;
  width: 108px;
  height: auto;
  opacity: 0.9;
}
@media (max-width: 760px) {
  .page-head {
    padding: 48px 0 40px;
  }
  .page-head__art {
    display: none;
  }
}
.page-head + section {
  padding-top: 64px;
}

/* --- filters ------------------------------------------------------------ */

.filter-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: 48px;
}
.filter {
  padding: 9px 20px;
  border-radius: 999px;
  border: 1px solid var(--hairline);
  background: transparent;
  color: var(--ink);
  font: 400 0.88rem/1 var(--ui);
  text-decoration: none;
  transition:
    border-color 0.2s ease,
    color 0.2s ease;
}
.filter:hover {
  border-color: var(--ink);
}
.filter[aria-current='true'] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}
.filter-count {
  margin-left: auto;
  font-size: 0.84rem;
  color: var(--muted);
}
@media (max-width: 560px) {
  .filter-count {
    margin-left: 2px;
  }
}

/* --- signup ------------------------------------------------------------- */

.signup {
  background: var(--blush);
  border-radius: var(--radius-lg);
  padding: 72px 44px;
  text-align: center;
}
/* a striped trim rather than a striped field — the pattern should never sit
   under the copy */
.signup::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 14px;
  background: var(--stripes);
}
.signup h2 {
  margin-bottom: 10px;
}
.signup .lede {
  margin: 0 auto;
}
.signup form {
  display: flex;
  gap: 10px;
  max-width: 440px;
  margin: 30px auto 0;
}
.signup input {
  flex: 1;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  padding: 14px 22px;
  font: 400 0.94rem/1 var(--ui);
  color: var(--ink);
  background: var(--white);
}
.signup input:focus {
  outline: none;
  border-color: var(--raspberry);
}
.signup .status {
  margin: 16px 0 0;
  font-size: 0.88rem;
  min-height: 1.2em;
  color: var(--muted);
}
/* The launch-list form answers a no-JavaScript submit by redirecting back with
   the outcome pre-rendered into .status. A failure has to read as one without
   relying on colour alone, so it is also the only weighted line here. */
.signup .status--warn {
  color: var(--ink);
  font-weight: 500;
}
@media (max-width: 560px) {
  .signup {
    padding: 48px 24px;
  }
  .signup form {
    flex-direction: column;
  }
}

/* --- empty catalogue ---------------------------------------------------- */

.empty-shelf {
  text-align: center;
  padding: 72px 28px 80px;
  background: var(--paper-warm);
  border-radius: var(--radius-lg);
}
.empty-shelf img {
  height: 86px;
  width: auto;
  margin: 0 auto 24px;
  opacity: 0.85;
}
.empty-shelf .lede {
  margin: 0 auto 28px;
  max-width: 44ch;
}

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

.site-footer {
  position: relative;
  background: var(--paper-warm);
  padding: 78px 0 36px;
  margin-top: 40px;
}
/* a checkerboard trim, like the edge of a tablecloth */
.site-footer::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 16px;
  background: var(--checks);
  background-size: 32px 32px;
}
.site-footer .cols {
  display: grid;
  grid-template-columns: 1.5fr repeat(3, 1fr);
  gap: 48px;
}
@media (max-width: 860px) {
  .site-footer .cols {
    grid-template-columns: 1fr 1fr;
    gap: 36px;
  }
}
.site-footer img.lockup {
  height: 86px;
  width: auto;
  margin-bottom: 18px;
}
.site-footer .col-title {
  font-size: 0.7rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 18px;
  font-weight: 600;
}
.site-footer ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
.site-footer li {
  margin-bottom: 11px;
}
.site-footer a {
  color: var(--ink);
  text-decoration: none;
  font-size: 0.9rem;
}
/* On a phone these lists are the whole site map, and each row was a 20px-tall
   target with 11px between it and the next — small enough to mis-tap and to
   fail WCAG 2.2's 24x24 minimum. Padding, not a bigger font, so the footer
   still looks like a footer. */
@media (max-width: 860px) {
  .site-footer li {
    margin-bottom: 0;
  }
  .site-footer li a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
}
.site-footer a:hover {
  color: var(--raspberry);
}
.colophon {
  margin-top: 56px;
  padding-top: 24px;
  border-top: 1px solid var(--hairline);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 28px;
  justify-content: space-between;
  font-size: 0.79rem;
  color: var(--muted);
}
.footer-badge {
  height: 58px;
  width: auto;
  opacity: 0.85;
}

/* --- content pages ------------------------------------------------------ */

.prose-col {
  max-width: 720px;
}
.prose h2 {
  margin-top: 1.9em;
  font-size: 1.5rem;
}
.prose p {
  color: var(--muted);
}
.prose ul {
  padding-left: 20px;
}
.prose li {
  margin-bottom: 8px;
}

.licence {
  margin-top: 52px;
  padding-top: 32px;
  border-top: 1px solid var(--hairline);
}
.licence h2 {
  font-size: 1.25rem;
}

/* --- 404 ---------------------------------------------------------------- */

.not-found {
  padding: 110px 0 130px;
  text-align: center;
}
.not-found img {
  height: 104px;
  width: auto;
  margin: 0 auto 28px;
  opacity: 0.9;
}

/* --- transactional pages ------------------------------------------------ */

.panel {
  max-width: 620px;
  margin: 64px auto 0;
  background: var(--white);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  padding: 44px;
  box-shadow: var(--lift);
}
.panel h1 {
  font-size: 1.7rem;
}
.panel .sub {
  color: var(--muted);
  margin-top: -6px;
}
ul.files {
  list-style: none;
  margin: 26px 0;
  padding: 0;
}
ul.files li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 0;
  border-bottom: 1px solid var(--hairline);
}
ul.files li:last-child {
  border-bottom: 0;
}
.fmeta {
  display: block;
  font-size: 0.82rem;
  color: var(--muted);
}
.tag {
  display: inline-block;
  font-size: 0.63rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  /* The board's olive at 10px on white is 2.94:1. This is the label that tells
     a buyer which file to open first, on the page they land on after paying,
     so it has to be readable — the hue is kept and the value dropped until it
     clears 4.5:1 (now 5.12 on --white, 4.93 on --paper). */
  color: color-mix(in srgb, var(--olive) 65%, var(--ink));
  margin-left: 10px;
  vertical-align: middle;
}
.notice {
  background: var(--paper-warm);
  border-radius: var(--radius);
  padding: 18px 20px;
  margin: 0 0 24px;
  font-size: 0.9rem;
}
.notice > strong {
  display: block;
  margin-bottom: 8px;
}
.notice ul {
  margin: 0;
  padding-left: 18px;
  color: var(--muted);
}
.field {
  display: flex;
  gap: 10px;
  margin-top: 22px;
}
.field input {
  flex: 1;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  padding: 13px 20px;
  font: 400 0.94rem/1 var(--ui);
  background: var(--white);
  color: var(--ink);
}
.field input:focus {
  outline: none;
  border-color: var(--raspberry);
}
.panel-back {
  text-align: center;
  margin: 26px 0 56px;
  font-size: 0.86rem;
}
.panel-back a {
  color: var(--muted);
  text-decoration: none;
}
.panel-back a:hover {
  color: var(--raspberry);
}

/* --- placeholder captioning -------------------------------------------- */

.ph-legend {
  display: block;
  font-size: 0.75rem;
  color: var(--muted);
  letter-spacing: 0.01em;
}

/* --- reveal on scroll: the one piece of motion -------------------------- */

/* Scoped to `.js`, which an inline script in the document head sets before
   first paint (see views/layout.ts).

   Without that scope these rules hid the entire site from anyone with
   JavaScript off or blocked, and from any script that failed to load: every
   section starts at opacity 0 and only the IntersectionObserver in site.js
   ever clears it. The markup was there, correct and readable to a crawler,
   and invisible to a person. Progressive enhancement means the effect is
   opt-in for browsers that can run it, never a precondition for seeing the
   page — which the sign-in and download flows in particular must not have. */
.js .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition:
    opacity 0.7s ease,
    transform 0.7s cubic-bezier(0.2, 0.7, 0.3, 1);
}
.js .reveal.in {
  opacity: 1;
  transform: none;
}

/* The failsafe, and why it is not optional.
 *
 * `.js` is set by an inline script before first paint, but the thing that ever
 * removes `opacity: 0` is the IntersectionObserver in site.js — a separate
 * request. If that one file fails to arrive while the stylesheet did, every
 * .reveal section stays invisible forever. On /signin that is the entire page:
 * the whole form is inside one .reveal.
 *
 * site.js sets `.reveal-ready` on <html> as its first act, so in the normal
 * case this rule does not apply at all and the scroll reveal behaves exactly
 * as designed. Only when site.js never ran does the animation fire, three
 * seconds in, and show everything. Slower than the reveal, and infinitely
 * faster than never. This is the same failure the repo already met once, when
 * `.reveal` was unscoped and hid the site from anyone without JavaScript. */
.js:not(.reveal-ready) .reveal {
  animation: reveal-failsafe 0.4s ease 3s forwards;
}
@keyframes reveal-failsafe {
  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto;
  }
  .js .reveal {
    opacity: 1;
    transform: none;
  }
}

/* --- accounts: sign-in and the shelf ------------------------------------ */

/* A stacked form. Used for sign-in, which is two fields at most and reads
   better as a column than as the storefront's inline signup row. */
.stack {
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 420px;
  margin: 28px 0 0;
}
.stack--tight {
  margin-top: 14px;
}
.stack label {
  font: 500 0.86rem/1 var(--ui);
  color: var(--muted);
  letter-spacing: 0.02em;
}
.stack input {
  border: 1px solid var(--hairline);
  border-radius: 999px;
  padding: 14px 22px;
  font: 400 0.98rem/1 var(--ui);
  color: var(--ink);
  background: var(--white);
}
.stack input:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--accent) 70%, var(--ink));
  outline-offset: 2px;
}
/* The six-digit code. Spaced and tabular so a mistyped digit is obvious. */
.stack input#code {
  font-size: 1.5rem;
  letter-spacing: 0.4em;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.micro {
  font: 400 0.82rem/1.6 var(--ui);
  color: var(--muted);
  margin: 6px 0 0;
}

/* A button that has to look like a link, because "send a new code" and
   "sign out" must be POSTs — a GET that changes state is a bug waiting for a
   prefetching browser to find it. */
.linkish {
  background: none;
  border: 0;
  padding: 0;
  font: 400 0.88rem/1.6 var(--ui);
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.linkish:hover {
  color: var(--raspberry-deep);
}

.shelf {
  list-style: none;
  margin: 28px 0 0;
  padding: 0;
}
.shelf__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 22px;
  padding: 22px 0;
  border-bottom: 1px solid var(--hairline);
}
.shelf__item:first-child {
  border-top: 1px solid var(--hairline);
}
.shelf__title {
  font: 500 1.08rem/1.3 var(--ui);
  color: var(--ink);
  text-decoration: none;
}
.shelf__title:hover {
  color: var(--accent);
}
.shelf__item .sub {
  margin: 4px 0 0;
  font: 300 0.92rem/1.5 var(--ui);
  color: var(--muted);
}
.shelf__state {
  font: 500 0.84rem/1 var(--ui);
  color: var(--muted);
  white-space: nowrap;
}

@media (max-width: 620px) {
  .shelf__item {
    flex-direction: column;
    align-items: flex-start;
    gap: 14px;
  }
}
