/* Craftery — the shell.
 *
 * The two typefaces, base element styles, the header and footer every page
 * shares, the button voice, and the motion primitives. Every value comes from
 * tokens.css, linked immediately before this file.
 */

/* ---- Display · Fraunces ---------------------------------------------------
   Headings and the wordmark. Self-hosted, subset to the 95 glyphs heading
   copy uses, 9.2 KB. A single static instance of the variable family — opsz
   60 for a display-scale texture, wght 600, SOFT 0, WONK 0 (the wonky
   alternate axis off, for a confident rather than quirky headline).

   The same family the 3D text engine prints with, which is a quiet piece of
   coherence: the headline above the object and the name cut into the object
   are one typeface. */
@font-face {
  font-family: 'Fraunces Display';
  src: url('/fonts/fraunces-display.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  unicode-range: U+20-7A, U+100-101, U+112-113, U+12A-12B, U+14C-14D, U+16A-16B, U+2013-2014, U+2018-2019, U+201C-201D, U+2026;
}

/* ---- Body · Instrument Sans -----------------------------------------------
   Everything that is not a heading. One variable file clamped to wght
   400-700 with the width axis pinned, 37.5 KB. Rebuild both with
   `pnpm subset-web-fonts`. */
@font-face {
  font-family: 'Instrument Sans';
  src: url('/fonts/instrument-sans.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

/* Metric-matched local faces, so nothing reflows when the webfonts land.
   Measured from the fonts: Instrument Sans is 1000 units/em, hhea ascent 970,
   descent -250, gap 0, x-height 510 (0.510 against Arial's 0.5186 → 98.35%).
   Fraunces display is only ever set large and short, so its fallback is
   Georgia — the one serif present on both Windows and macOS with a
   comparable x-height. */
@font-face {
  font-family: 'Instrument Fallback';
  src: local('Arial'), local('Helvetica Neue'), local('Segoe UI');
  size-adjust: 98.35%;
  ascent-override: 97%;
  descent-override: 25%;
  line-gap-override: 0%;
}

@font-face {
  font-family: 'Fraunces Fallback';
  src: local('Georgia'), local('Times New Roman');
  size-adjust: 97%;
}

/* ---- Base -----------------------------------------------------------------
   `overflow-x: clip`, never `hidden`. The landing page runs a full-bleed
   marquee; `hidden` would contain it but also creates a scroll container,
   which breaks every `position: sticky` descendant — including the
   configurator stage on three routes. Both elements, because older Safari
   honours only `body`. */

html {
  overflow-x: clip;
  scroll-behavior: smooth;
}

body {
  overflow-x: clip;
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

* {
  box-sizing: border-box;
}

/* Removes the 300ms tap delay without disabling pinch-zoom, which
   `touch-action: none` at this level would. The 3D canvases set their own,
   because they genuinely consume the gesture. */
a, button, input, select, label, summary {
  touch-action: manipulation;
}

main {
  max-width: 1200px;
  margin: 0 auto;
  padding: var(--space-xl) var(--space-lg) var(--space-3xl);
}

/* Headings are Fraunces, set tight. Family, weight and tracking live here so
   every page reads as the same site; size and colour stay with each page. */
main h1, main h2, main h3 {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.08;
  /* A long unbroken word breaks inside itself rather than pushing the page
     sideways. */
  overflow-wrap: anywhere;
  min-width: 0;
}

main h1 { font-size: var(--text-2xl); margin: 0 0 var(--space-md); }
main h2 { font-size: var(--text-xl); margin: 0 0 var(--space-sm); }
main h3 { font-size: var(--text-md); margin: 0 0 var(--space-xs); }

p { max-width: var(--measure); }

/* Prices, quantities and dimensions line up as a column the eye can add, and
   stop the row reflowing every time a digit changes. */
.price, .cart-totals dd, .cart-qty, .text-input.qty {
  font-variant-numeric: tabular-nums;
}

a {
  color: var(--ink);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: color var(--dur-fast) var(--ease-out);
}

a:hover { color: var(--accent); }

/* ---- Buttons --------------------------------------------------------------
   One primary action per screen, filled in the accent. Everything else is an
   ink hairline. 44px min-height is the touch floor; `white-space: nowrap`
   because a label that wraps to two lines makes the target's bounds ambiguous
   and grows the button out of step with its siblings. */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-height: 44px;
  padding: var(--space-sm) var(--space-lg);
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.005em;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.btn--primary {
  background: var(--accent);
  color: var(--accent-ink);
  box-shadow: var(--shadow-sm);
}

.btn--primary:hover {
  background: var(--accent-deep);
  color: var(--accent-ink);
  box-shadow: var(--shadow-md);
  transform: translateY(-1px);
}

.btn--secondary {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--ink);
}

.btn--secondary:hover {
  border-color: var(--ink);
  background: var(--paper-3);
}

.btn:active { transform: translateY(1px); }

/* ---- Header ---------------------------------------------------------------
   Cream on cream with a hairline, not a white bar: the page should feel like
   one sheet of paper, and a contrasting header is a second surface arguing
   with it. Sticky, because the cart has to stay reachable — before this it
   was not reachable from a product page at all. */

.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  background: var(--paper);
  border-bottom: 1px solid var(--line);
}

/* Tier one: the wordmark and the four things a visitor does. The sticky and
   the background moved up to `.site-header` so the occasion row below is part
   of the same pinned block rather than scrolling out from under it. */
.site-header__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  max-width: 1200px;
  margin: 0 auto;
  padding: var(--space-md) var(--space-lg);
}

/* ---- Announcement bar ------------------------------------------------------
   The delivery cut-off, when one is running. Ink rather than the accent: it
   appears on every page of the site, and tokens.css is right that an accent
   covering a full-width strip on every screen has stopped meaning "press
   this". Scrolls away with the page — see Base.astro on why it is not a
   second sticky bar. */

.announce {
  background: var(--ink);
  color: var(--paper);
}

.announce__text {
  max-width: 1200px;
  margin: 0 auto;
  padding: var(--space-xs) var(--space-lg);
  text-align: center;
  font-size: var(--text-sm);
  /* `p` carries `max-width: var(--measure)` globally, which would strand this
     sentence in a 62ch column on the left of a wide strip. */
  max-inline-size: 1200px;
}

.announce__link {
  color: var(--paper);
  font-weight: 600;
  text-underline-offset: 3px;
}

.announce__link:hover,
.announce__link:focus-visible {
  color: var(--paper);
}

/* ---- Occasion bar ----------------------------------------------------------
   Tier two: eight ways into one product. A shop with a single product has
   nothing to put in a category nav, which is why the old header had three
   links and stopped; a nav of occasions has as many entries as there are
   reasons to buy.

   Scrolls sideways on a narrow screen rather than wrapping. Eight wrapped
   labels become three rows on a phone, pushing every page's content below the
   fold — and nothing is hidden behind a "more" control that a keyboard user
   would then have to discover. */

.occasion-bar {
  border-top: 1px solid var(--line);
  overflow-x: auto;
  /* Stops a sideways flick from also dragging the page up and down, which is
     what makes a scrolling strip feel broken on a touch screen. */
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}

.occasion-bar::-webkit-scrollbar {
  display: none;
}

.occasion-bar__list {
  display: flex;
  gap: var(--space-xs);
  list-style: none;
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 var(--space-lg);
}

.occasion-bar__item {
  flex: 0 0 auto;
}

.occasion-bar__link {
  display: inline-flex;
  align-items: center;
  /* 44px, the touch-target floor, reached with padding rather than a height
     so the text stays vertically centred when it is the only thing in the
     box. */
  min-height: 44px;
  padding: 0 var(--space-sm);
  color: var(--ink-2);
  font-size: var(--text-sm);
  text-decoration: none;
  white-space: nowrap;
  border-bottom: 2px solid transparent;
  transition: color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}

.occasion-bar__link:hover,
.occasion-bar__link:focus-visible {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

.wordmark {
  position: relative;
  display: inline-block;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-lg);
  letter-spacing: -0.03em;
  text-decoration: none;
  color: var(--ink);
  transition: color var(--dur-fast) var(--ease-out);
}

/* Extends the hit area past the visual bounds rather than padding the mark
   out to 44px, which would push the header taller than its content needs. */
.wordmark::after {
  content: '';
  position: absolute;
  inset: -10px -6px;
}

.wordmark:hover { color: var(--accent); }

/*
   The logo lockup, in place of the typeset wordmark.

   Sized by HEIGHT, not width: the mark sits on the same optical line as the
   nav links beside it, which is a cap-height relationship rather than a
   measure. `width: auto` then keeps the 3.86:1 lockup from being squashed if
   that height ever changes.

   The `width`/`height` attributes on the <img> stay at the intrinsic size so
   the browser reserves the correct box before the file arrives; these rules
   override the rendered size without giving up that reservation. Without it
   this is a layout shift at the very top of every first page view.
*/
.wordmark--logo {
  display: inline-flex;
  align-items: center;
}

.wordmark--logo img {
  display: block;
  height: 1.75rem;
  width: auto;
  /* No colour transition on hover. A brand mark keeps its own colour; the
     link signals hover with opacity instead. */
  transition: opacity var(--dur-fast) var(--ease-out);
}

.wordmark--logo:hover img { opacity: 0.72; }

/* Larger in the footer, where it anchors the band rather than sharing a line
   with navigation. */
.wordmark--footer img { height: 2.25rem; }

/* Forced colours leave images alone, so the mark survives as drawn. The hover
   fade is dropped there: in a high-contrast mode a faded mark reads as
   "disabled" rather than "hovered". */
@media (forced-colors: active) {
  .wordmark--logo:hover img { opacity: 1; }
}

.site-nav {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

.site-nav__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--space-md);
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
  color: var(--ink-2);
  white-space: nowrap;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.site-nav__link:hover,
.site-nav__link:focus-visible {
  background: var(--paper-2);
  color: var(--ink);
}

/* The cart is the one nav item that is a destination rather than a jump, so
   it carries a filled treatment. Ink rather than the accent: the accent is
   reserved for the action on the current screen, and a permanently-accented
   chrome element spends that meaning on nothing. */
.site-nav__link--cart {
  background: var(--ink);
  color: var(--paper);
}

.site-nav__link--cart:hover,
.site-nav__link--cart:focus-visible {
  background: var(--accent);
  color: var(--accent-ink);
}

/* The header's one narrow-screen rule.

   The bar is a single flex row of logo + four links with `white-space:
   nowrap` on each, which fits comfortably from about 560px up and does not
   fit at all below it: measured at 320, 375 and 414px, the row ran to 481px
   and put "Ideas", "Help" and the Cart off the right-hand edge. The page did
   not even scroll to reveal them — the header's own box clipped them — so on
   a phone the site had one reachable nav link.

   Wrapping to a second row rather than hiding anything. The alternative on
   offer was dropping "Design yours" below 30rem on the grounds that the hero,
   the occasion bar and every tile also lead there, which is true and still
   wrong: a nav that silently loses an item on the devices most people use is
   a nav that was designed for the desktop screenshot. Two rows cost about
   44px of header on the smallest phones and nothing above 35rem. */
@media (max-width: 35rem) {
  .site-header__bar {
    flex-wrap: wrap;
    row-gap: var(--space-2xs);
    padding-bottom: var(--space-xs);
  }

  .site-nav {
    /* Takes the whole second row and spreads across it, so the four targets
       are as wide apart as the screen allows rather than bunched at one
       edge. */
    width: 100%;
    justify-content: space-between;
    gap: var(--space-3xs);
  }

  .site-nav__link {
    padding: 0 var(--space-sm);
  }
}

/* The smallest phones still in use — an iPhone SE is 320 CSS px wide. Wrapping
   the bar bought enough room at 375 and up; at 320 the four links came to
   356px on their own row and "Cart" went over the edge again.

   The pill padding comes off rather than the font size: shrinking the label
   below `--text-sm` is how a nav becomes unreadable on exactly the device
   with the least room, and the padding is decoration that costs 24px a link.
   Each target stays at least 24x24 CSS px (WCAG 2.2 AA's floor) and keeps the
   full 44px of height — only the horizontal padding goes. */
@media (max-width: 22rem) {
  .site-nav__link {
    padding: 0 var(--space-2xs);
  }
}

/* ---- Footer ---------------------------------------------------------------
   An ink block that closes the page. The only large dark surface on the site,
   so it reads as an ending rather than as another section. */

.site-footer {
  margin-top: var(--space-3xl);
  background: var(--ink);
  color: var(--paper);
}

/* The index. Four columns of links above the brand band — where a first-time
   buyer checks whether a shop they have never heard of is real, and the only
   place on the site every page is reachable from every other. */
.site-footer__index {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-xl);
  max-width: 1200px;
  margin: 0 auto;
  padding: var(--space-2xl) var(--space-lg) var(--space-xl);
}

.site-footer__heading {
  margin: 0 0 var(--space-xs);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(250, 247, 242, 0.6);
}

.site-footer__list {
  list-style: none;
  margin: 0;
  padding: 0;
  /* The negative margin pulls each link's 44px hit area back off the column
     edge, so the touch targets touch without the visible text drifting
     inwards. */
  margin-inline: calc(var(--space-sm) * -1);
}

.site-footer__list a,
.site-footer__contact a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--space-sm);
  color: rgba(250, 247, 242, 0.82);
  font-size: var(--text-sm);
  text-decoration-thickness: 1px;
}

.site-footer__list a:hover,
.site-footer__list a:focus-visible,
.site-footer__contact a:hover,
.site-footer__contact a:focus-visible {
  color: var(--paper);
}

.site-footer__contact {
  margin: 0 0 var(--space-xs);
  max-width: 34ch;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: rgba(250, 247, 242, 0.7);
}

.site-footer__col--contact a {
  margin-inline: calc(var(--space-sm) * -1);
}

@media (max-width: 60rem) {
  .site-footer__index {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-lg);
  }
}

@media (max-width: 34rem) {
  .site-footer__index {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* The brand band, now the footer's closing line rather than the whole of it.
   A hairline above it separates the index from the signature. */
.site-footer__band {
  display: grid;
  grid-template-columns: minmax(0, auto) minmax(0, 1fr);
  gap: var(--space-xs) var(--space-xl);
  align-items: baseline;
  max-width: 1200px;
  margin: 0 auto;
  padding: var(--space-xl) var(--space-lg) var(--space-2xl);
  border-top: 1px solid rgba(250, 247, 242, 0.12);
}

.site-footer .wordmark { color: var(--paper); }
.site-footer .wordmark:hover { color: var(--accent); }

.site-footer__tagline {
  margin: 0;
  max-width: 44ch;
  font-size: var(--text-md);
}

.site-footer__meta {
  grid-column: 2;
  margin: var(--space-xs) 0 0;
  /* The cream at 70% on ink still measures well over the floor, and reads as
     secondary without introducing a fourth grey. */
  color: rgba(250, 247, 242, 0.7);
  font-size: var(--text-sm);
}

/* The policy row. Explicitly `grid-column: 2` rather than auto-placed: the
   band is a two-column grid and `__meta` above forces column 2, which leaves
   the auto-placement cursor at the start of the next row — so an unpositioned
   nav lands under the *wordmark* instead of lining up with the prose it
   belongs with.

   Each link carries its own 44px hit area via padding rather than a pseudo
   element, because unlike the header nav these sit in a wrapping flex row
   where a grown hit area would overlap its neighbour. The row's negative
   margin pulls the padding back off the band's own edges so the visible
   spacing is unchanged. */
.site-footer__links {
  grid-column: 2;
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--space-lg);
  margin: var(--space-sm) calc(var(--space-sm) * -1) 0;
}

.site-footer__links a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--space-sm);
  color: rgba(250, 247, 242, 0.7);
  font-size: var(--text-sm);
  text-decoration-thickness: 1px;
}

.site-footer__links a:hover,
.site-footer__links a:focus-visible { color: var(--paper); }

@media (max-width: 40rem) {
  .site-footer__band { grid-template-columns: minmax(0, 1fr); gap: var(--space-xs); }
  .site-footer__meta { grid-column: 1; }
  .site-footer__links { grid-column: 1; }
}

/* ---- Motion ---------------------------------------------------------------
   Two primitives. `.reveal` is a section arriving; `.stagger > *` is a grid
   arriving as a wave. Both fade and rise — no scale, no overshoot: an elastic
   curve on a gift site reads as a toy.

   Both are gated on `[data-reveal-ready]`, set by an inline script before
   first paint, so a reader with JavaScript off gets the content at full
   opacity rather than a page that never fills in. A three-second timeout in
   that script reveals anything the observer missed, because the failure mode
   of this pattern is not a missed animation — it is content nobody can see. */

[data-reveal-ready] .reveal,
[data-reveal-ready] .stagger > * {
  opacity: 0;
  transform: translateY(14px);
}

[data-reveal-ready] .reveal[data-revealed],
[data-reveal-ready] .stagger[data-revealed] > * {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
}

[data-reveal-ready] .stagger[data-revealed] > * {
  transition-delay: calc(var(--i, 0) * var(--stagger-step));
}

/* ---- Accessibility --------------------------------------------------------
   The focus ring appears instantly and is never transitioned: a ring that
   fades in leaves a keyboard user with no indicator for the first fifth of a
   second, which is exactly when they need it. Ink rather than the accent, so
   it stays visible when the focused thing IS the accent. */

:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* First focusable element in the document. Off-screen via transform rather
   than display:none, which would also remove it from the tab order. */
.skip-link {
  position: absolute;
  top: var(--space-xs);
  left: var(--space-xs);
  transform: translateY(-150%);
  background: var(--ink);
  color: var(--paper);
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-sm);
  font-weight: 600;
  text-decoration: none;
  z-index: var(--z-toast);
  transition: transform var(--dur-fast) var(--ease-out);
}

.skip-link:focus { transform: translateY(0); }

/* ---- Reduced motion -------------------------------------------------------
   Spatial motion goes entirely. The colour transitions stay: each is 160ms
   and carries state rather than decorating it. */

@media (prefers-reduced-motion: reduce) {
  [data-reveal-ready] .reveal,
  [data-reveal-ready] .stagger > * {
    transform: none;
  }

  [data-reveal-ready] .reveal[data-revealed],
  [data-reveal-ready] .stagger[data-revealed] > * {
    transition: opacity var(--dur-fast) linear;
    transition-delay: 0ms;
  }

  .btn:active,
  .btn--primary:hover {
    transform: none;
  }
}
