/* Landing-page-only styles. global.css defines the palette tokens this file
   styles against (--ink, --ink-2, --ink-2,
   --paper-3, --accent, --accent-deep, --focus-ring, --font-display) — the
   two tokens below extend that set, scoped to this file because nothing
   outside the landing page reads them yet. */
:root {

  /* The on-screen rhythm standing in for the real thing: FDM printing lays
     material down in 0.2 mm bands (LAYER_HEIGHT_MM in
     apps/web/src/islands/tagScene.ts, which shades the 3D preview with
     exactly this banding). 0.2 mm is sub-pixel at any screen density, so
     this is not to scale — it is the same idea at a size a screen can
     actually show: a repeating hairline, the honest texture of the thing
     this business makes, not a stripe pattern picked for its own sake. */
  --layer-pitch: 5px;
}

/* ---- Rotating hero --------------------------------------------------------
   The signature element: a colour that will not sit still, shifting between
   blue-green and violet the way oil on water or a hologram foil does.

   It began as a tūī's plumage, from when the business was named after the
   bird, and the hues in global.css are the ones that came from it. The effect
   outlived the name because it earns its place on its own — it is the one
   thing on this page that moves without demanding to be looked at — but it no
   longer stands in for anything. It is a gradient now, not a reference.

   Two corrections from the previous pass, made after looking at this at
   1440×900 rather than trusting the reasoning alone:

   1. The full-bleed repeating-line texture that used to sit behind the
      whole hero (a stand-in for the printer's own 0.2 mm bands) read as CRT
      scanlines at this scale and contrast, not as print texture — hard
      horizontal banding across the entire box. Removed outright rather than
      dimmed: dimming a pattern that is legible as discrete lines only
      changes how visible the lines are, not whether they still read as
      lines. The layer-line *idea* now lives in `.hero-carousel__visual--
      rings` below, as concentric bands rather than horizontal ones — a
      shape that cannot be mistaken for a display artefact.
   2. The wash swept through three hues (teal → violet → magenta) at full
      strength across the entire box, which produced a saturated magenta
      lobe competing with the product photo beside it — "a rainbow", not the
      subtle, angle-dependent shift the brief asked for. Narrowed to the two
      colours the base gradient already uses (no third, more saturated stop)
      and masked to a soft circle so the shift is felt as a gentle sheen in
      one area, not painted uniformly over the whole hero. */

.hero-carousel {
  position: relative;
  /* Contains the negative-z-index wash layer below so it can never stack
     below page content that comes after the hero in the DOM, and keeps
     `::after`'s `mix-blend-mode: color` blending only against this box's
     own background rather than bleeding through to whatever the page
     happens to render behind it. */
  isolation: isolate;
  border-radius: 1rem;
  overflow: hidden;
  background: linear-gradient(135deg, var(--ink), var(--ink-2));
  color: var(--paper-3);
}

/* The iridescent wash. `mix-blend-mode: color` takes only the hue and
   saturation of this layer and keeps the backdrop's own luminance — the
   backdrop here is always the dark teal-to-violet gradient above, so this
   can shift hue as freely as it likes (by the idle drift below, or by
   --iris-x/--iris-y from the pointer) without ever being able to lighten
   the surface --paper-3 text sits on. Masking the layer with a radial
   gradient (rather than lowering its opacity) preserves that guarantee
   exactly through the soft edge too: relative luminance is a linear
   function of a colour's channels, alpha compositing is a linear
   interpolation of those same channels, and both the masked-in wash and the
   masked-out backdrop share the backdrop's luminance at every pixel — so
   every point in between does too, by construction, not by re-testing every
   radius. `from 200deg` and the position defaults (68%/34%, reset on
   pointer leave by index.astro's script) aim the sheen somewhere
   unobtrusive — behind the copy on a wide screen — before any pointer has
   moved. */
.hero-carousel::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: conic-gradient(
    from 200deg at var(--iris-x, 68%) var(--iris-y, 34%),
    var(--ink) 0deg,
    var(--ink-2) 180deg,
    var(--ink) 360deg
  );
  mix-blend-mode: color;
  mask-image: radial-gradient(
    circle at var(--iris-x, 68%) var(--iris-y, 34%),
    #000 0%,
    #000 20%,
    transparent 65%
  );
  -webkit-mask-image: radial-gradient(
    circle at var(--iris-x, 68%) var(--iris-y, 34%),
    #000 0%,
    #000 20%,
    transparent 65%
  );
  animation: iris-drift 32s ease-in-out infinite;
}

/* Idle motion when nothing is being pointed at: a slow ±8deg hue-rotate
   (down from the previous ±14deg, matching the calmer wash above), the
   same "turn your head and it shifts" read the pointer gives a mouse user,
   for everyone else (touch, keyboard-only, a desktop visitor who hasn't
   moved the pointer over the hero yet). Killed outright, not slowed, under
   reduced motion — see the media query below, matching the carousel's own
   timer rule in HeroCarousel.tsx. */
@keyframes iris-drift {
  0%, 100% { filter: hue-rotate(0deg); }
  50% { filter: hue-rotate(8deg); }
}

@media (prefers-reduced-motion: reduce) {
  .hero-carousel::after {
    animation: none;
  }
}

.hero-carousel__frame {
  min-height: 20rem;
  display: flex;
  align-items: center;
}

.hero-carousel__slide {
  display: grid;
  /* Visual panel narrower than copy (was the reverse) — the panel now has a
     fixed aspect-ratio (see .hero-carousel__visual below) rather than an
     intrinsic image height, so a wide column would make it tall enough to
     dominate the slide on every text-only variant, not just the one photo
     slide. */
  grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
  align-items: center;
  gap: 2rem;
  width: 100%;
  padding: 3rem clamp(1.5rem, 5vw, 4rem);
  /* Cross-fade on entry. Each new slide is a freshly mounted <article> (see
     HeroCarousel.tsx's `key={slide.id}`), so this animation restarts on
     every advance without any JS-driven transition state. */
  animation: hero-carousel-fade-in 500ms ease both;
}

@media (max-width: 48rem) {
  .hero-carousel__slide {
    grid-template-columns: 1fr;
    padding: 2.5rem 1.5rem;
    text-align: center;
  }
}

@keyframes hero-carousel-fade-in {
  from { opacity: 0; transform: translateY(0.5rem); }
  to { opacity: 1; transform: translateY(0); }
}

/* Every transition/animation this stylesheet defines, killed in one place
   under reduced motion — the carousel's own cross-fade, the CTA's hover
   lift, and the category card's pre-existing hover lift alike. Consolidated
   here rather than scattered next to each rule so this list stays a single
   thing to audit against the non-negotiable it exists to satisfy. */
@media (prefers-reduced-motion: reduce) {
  .hero-carousel__slide {
    animation: none;
  }
  .hero-carousel__cta {
    transition: none;
  }
  .category-card__link {
    transition: none;
  }
  .category-card__link:hover,
  .category-card__link:focus-visible {
    transform: none;
  }
}

/* ---- Hero · the shop window -----------------------------------------------
   Copy on the left, the live product on the right. The object is given the
   larger column: it is the thing being sold, and it is the only part of this
   page a competitor cannot copy by rewriting their headline.

   Asymmetric (0.85 / 1.15) rather than split down the middle — equal halves
   read as a layout with nothing to say about which side matters. */

.hero {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: var(--space-2xl);
  align-items: center;
  padding: var(--space-xl) 0 var(--space-2xl);
}

.hero__heading {
  font-family: var(--font-display);
  font-size: var(--text-display);
  font-weight: 600;
  line-height: 1.02;
  letter-spacing: -0.03em;
  margin: 0 0 var(--space-lg);
  overflow-wrap: anywhere;
  min-width: 0;
}

.hero__lede {
  margin: 0;
  max-width: 38ch;
  font-size: var(--text-md);
  line-height: 1.55;
  color: var(--ink-2);
}

.hero-preview {
  display: grid;
  gap: var(--space-lg);
}

/* The stage inherits its frame from configurator.css; this only sets the
   proportion the hero wants and stops it sticking, which is a product-page
   behaviour that makes no sense in a hero. */
.hero-preview__stage {
  position: relative;
  aspect-ratio: 5 / 4;
  top: auto;
}

.hero-preview__controls {
  display: grid;
  gap: var(--space-xs);
}

.hero-preview__label {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-2);
}

/* The one input on the page, set at display size: typing the name IS the
   product, so the field should feel like the main event rather than a
   newsletter signup. */
.hero-preview__input {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  width: 100%;
}

.hero-preview__hint {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--ink-3);
}

.hero-preview__cta {
  justify-self: start;
  margin-top: var(--space-xs);
}

/* The demoted carousel. It keeps its own styling below; this just gives it
   room and stops it competing with the hero above. */
.hero-secondary {
  margin-top: var(--space-2xl);
}

@media (max-width: 60rem) {
  .hero {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-xl);
    padding-bottom: var(--space-xl);
  }

  /* The object goes first on a phone. The headline is good, but the thing
     that explains this business in one second is the tag itself. */
  .hero__copy { order: 2; }
  .hero-preview { order: 1; }

  .hero-preview__stage { aspect-ratio: 4 / 3; }
}

/* ---- Hero visual panel -----------------------------------------------
   The structural fix for the layout shift the client flagged: every slide
   renders exactly one of these, at the same fixed shape
   (`aspect-ratio: 4 / 3`, the same ratio the stand photos themselves were
   shot at), whether it holds a real photo or one of the two generated
   graphics below. Because the panel's height is now derived purely from
   its own width (via aspect-ratio) rather than from an image's intrinsic
   dimensions, it is identical across all four slides at a given viewport
   width — switching slides can no longer resize the row underneath the
   copy, which is what the previous "fixed left panel that collapses when
   absent" version did. See HeroCarousel.tsx's own comment on why a
   generated graphic, never a borrowed photo, fills this when there is no
   product shot. */
.hero-carousel__visual {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  border-radius: 0.75rem;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

.hero-carousel__visual--photo {
  background: rgba(27, 27, 27, 0.15);
  box-shadow: 0 0 0 1px rgba(251, 250, 247, 0.18);
}

.hero-carousel__image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The rings graphic: the layer-line idea, corrected. Concentric bands read
   as a printer's own cross-section (echoing `.how-steps__number` below,
   scaled up into the page's other moving slide) without ever being
   mistakable for horizontal scanlines, because they are circles, not full-
   width rows. Deliberately left with no panel background of its own — the
   ring sits directly on the hero's own gradient/wash, as a watermark rather
   than a boxed illustration, so it reads as part of the hero rather than a
   second competing graphic bolted beside the copy. */
.hero-carousel__visual--rings {
  background: none;
}

.hero-carousel__rings {
  display: block;
  width: min(62%, 13rem);
  aspect-ratio: 1;
  border-radius: 50%;
  background: repeating-radial-gradient(
    circle at 50% 50%,
    var(--paper-3) 0,
    var(--paper-3) 2px,
    transparent 2px,
    transparent var(--layer-pitch)
  );
  opacity: 0.55;
}

/* The nameplate graphic: a mock raised-text nameplate, the same device
   `.category-card__placeholder-tag` below uses and for the same reason —
   no photograph of a name tag exists, so this renders the actual thing the
   business makes (two-colour raised lettering) rather than borrowing a
   different product's photo. Its own gradient (steeper, more violet) reads
   as a distinct object sitting on the hero, not a continuation of the hero
   backdrop behind it. */
.hero-carousel__visual--nameplate {
  background: linear-gradient(155deg, var(--ink-2), var(--ink));
  box-shadow: inset 0 0 0 1px rgba(251, 250, 247, 0.18);
}

.hero-carousel__nameplate-text {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(1.8rem, 4vw, 2.75rem);
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--paper-3);
  transform: rotate(-3deg);
  text-shadow: 2px 3px 0 var(--ink), 4px 8px 14px oklch(22.2% 0 90 / 0.4);
}

.hero-carousel__copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.75rem;
}

@media (max-width: 48rem) {
  .hero-carousel__copy {
    align-items: center;
  }
}

.hero-carousel__heading {
  /* font-family/weight/letter-spacing come from global.css's `main h1, h2,
     h3` rule (Fraunces Display) — only size and line-height are specific
     to the hero's own scale. Pushed up from the previous pass's clamp(1.85,
     4vw, 2.75rem): the hero is the page's thesis statement, and at the old
     scale it read as one heading among several rather than the dominant
     line on the page. -0.02em on top of global.css's -0.01em base — a
     display face this size wants the extra bit of tightening a body-scale
     heading doesn't. */
  margin: 0;
  font-size: clamp(2.25rem, 6vw, 4.25rem);
  line-height: 1.02;
  letter-spacing: -0.02em;
}

.hero-carousel__body {
  margin: 0;
  max-width: 32rem;
  font-size: 1.05rem;
  color: var(--paper-3);
  opacity: 0.92;
  /* Reserves room for the longest of the four slides' body copy (three
     lines at this font-size/line-height) so a shorter slide's body text
     does not leave the row shorter than a longer one's — the last source
     of hero-height variance once the visual panel itself became a fixed
     size (see .hero-carousel__visual). */
  min-height: 4.9rem;
}

.hero-carousel__cta {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-top: var(--space-xs);
  padding: var(--space-sm) var(--space-lg);
  border-radius: var(--radius-sm);
  /* The orange half of the duotone. It was a white pill with green text at
     3.77:1 — under the floor, and it hid the one urgent action on the page
     inside the same colour family as everything around it. */
  background: var(--accent);
  color: var(--accent-ink);
  font-family: var(--font-display);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  box-shadow: var(--shadow-md);
  transition:
    background-color var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out);
}

.hero-carousel__cta:hover,
.hero-carousel__cta:focus-visible {
  background: var(--accent-deep);
  color: var(--accent-ink);
  box-shadow: var(--shadow-lg);
  transform: translateY(-1px);
}

/* Same problem as .hero-carousel__nav's own :focus-visible rule below, same
   fix, for the primary call to action: the global :focus-visible rule's
   --focus-ring (--ink, an emerald) measures 1.46:1 against the
   darkest point of this gradient — nowhere near the 3:1 UI-component floor
   — so without this override a keyboard user gets a clear ring on prev/next
   and essentially nothing on the CTA itself. The pause/play toggle
   (HeroCarousel.tsx) reuses the `.hero-carousel__nav` class list directly
   rather than getting its own rule, so it already inherits the fix below. */
.hero-carousel__cta:focus-visible {
  outline: 2px solid var(--paper-3);
  outline-offset: 2px;
}

.hero-carousel__controls {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 1rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
}

.hero-carousel__nav {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  border: 1px solid rgba(251, 250, 247, 0.5);
  background: rgba(27, 27, 27, 0.2);
  color: var(--paper-3);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
}

.hero-carousel__nav:hover {
  background: rgba(27, 27, 27, 0.35);
}

/* :focus-visible on this dark surface needs its own outline colour: the
   global rule's --focus-ring (--ink, an emerald) does not clear
   3:1 against this same-family gradient background, so the hero's controls
   use the light --paper-3 token instead — still a token, not a new
   literal, and it is the one colour in the palette guaranteed to contrast
   against both --ink and --ink-2. */
.hero-carousel__nav:focus-visible {
  outline: 2px solid var(--paper-3);
  outline-offset: 2px;
}

.hero-carousel__status {
  margin: 0;
  min-width: 4rem;
  text-align: center;
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: var(--paper-3);
  opacity: 0.85;
}

/* At this width the heading's clamp() has already hit its floor (2.25rem)
   regardless of slide, and reserving room for the longest slide's four
   lines (the same trick .hero-carousel__body above uses at desktop widths)
   left a visibly empty gap under every shorter slide's heading — worse to
   look at than the modest height difference it was meant to remove. Left
   alone deliberately: the one part of the shift the client actually
   flagged (the fixed-width image panel collapsing to nothing) is fixed by
   `.hero-carousel__visual` above at every width, including this one — see
   the hero-fix-report for the measured before/after numbers here. */

/* ---- Hero additions -------------------------------------------------------
   The price and the call to action, which the hero did not carry while the
   only way to buy was to scroll to a grid. A shop whose front page never
   names a price asks the visitor to work for it. */

.hero__price {
  margin: var(--space-md) 0 0;
  font-size: var(--text-md);
  color: var(--ink-2);
}

.hero__price strong {
  font-size: var(--text-lg);
  color: var(--ink);
}

.hero__cta {
  margin-top: var(--space-lg);
}

/* ---- Trust bar -------------------------------------------------------------
   Three claims in a flat band, directly under the fold. Modelled on the
   reference shop's two-item strip, with the saturation taken out: there the
   bar is a second accent block competing with the banner above it, and here
   the one bold thing on the page has to stay the product.

   `--paper-2` (the sunken surface) rather than a tint of the accent, so it
   reads as a shelf the claims sit on. */

.trust {
  margin-top: var(--space-xl);
  padding: var(--space-lg) var(--space-xl);
  background: var(--paper-2);
  border-radius: var(--radius-lg);
}

.trust__list {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-xl);
  margin: 0;
}

.trust__item {
  min-width: 0;
}

.trust__term {
  font-weight: 600;
  font-size: var(--text-base);
  color: var(--ink);
}

.trust__detail {
  margin: var(--space-2xs) 0 0;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--ink-2);
  max-width: none;
}

@media (max-width: 52rem) {
  .trust {
    padding: var(--space-lg);
  }

  .trust__list {
    grid-template-columns: 1fr;
    gap: var(--space-md);
  }
}

/* ---- Band heads ------------------------------------------------------------
   Every product band on this page opens the same way, so the rule is written
   once. Left-aligned rather than centred: the reference shop centres its
   band headings, which works when each is followed by a twelve-item carousel
   and the heading is a label on a conveyor belt. Here each band is a
   statement with a grid under it, and a centred heading over a left-aligned
   grid leaves the eye starting in two different places. */

.band-head {
  margin-bottom: var(--space-lg);
}

.band-head__intro {
  margin: var(--space-2xs) 0 0;
  color: var(--ink-3);
  max-width: 48ch;
}

/* ---- Occasions -------------------------------------------------------------
   Eight tiles, four across. NOT the reference shop's round pucks: theirs sit
   over square product photographs, and a keychain is about three times as
   wide as it is tall, so a circle would either crop the name off both ends or
   shrink the product to the middle fifth of the tile. A rounded rectangle is
   the same device adapted to the shape of the thing being sold.

   `minmax(0, 1fr)` rather than `1fr`: these tracks contain images, and a bare
   `1fr` is `minmax(auto, 1fr)`, whose `auto` floor is the image's intrinsic
   width — which here is up to 300px and would push the grid wider than its
   container on a phone. */

.occasions {
  margin-top: var(--space-3xl);
}

.occasion-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-lg);
}

.occasion-tile {
  min-width: 0;
}

.occasion-tile__link {
  display: grid;
  gap: var(--space-xs);
  height: 100%;
  align-content: start;
  padding: var(--space-md);
  border-radius: var(--radius-lg);
  background: var(--paper-3);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-sm);
  color: inherit;
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}

.occasion-tile__link:hover,
.occasion-tile__link:focus-visible {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
  border-color: var(--line-strong);
  color: inherit;
}

/* The stage the drawing sits on. Sunken, so the product reads as an object
   on a surface rather than as a sticker floating on a card. */
.occasion-tile__art {
  display: grid;
  place-items: center;
  aspect-ratio: 4 / 3;
  padding: var(--space-md);
  border-radius: var(--radius-md);
  background: var(--paper-2);
}

/* `contain`, never `cover`: these drawings are the product, and cropping a
   name in half to fill a box is the one thing this picture may not do.

   The drop shadow is not decoration. These are flat SVGs of a solid object,
   and the palest filaments — white, and lilac against a tinted stage — have
   almost no edge against the surface behind them, so the keychain reads as a
   word printed on the card rather than as a thing lying on it. `drop-shadow`
   follows the SVG's alpha channel, so it traces the real outline including
   the ring hole, which a `box-shadow` on the box could not. It is also what
   a photograph of the object would have. */
.occasion-tile__art img,
.colour-card__art img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: drop-shadow(0 2px 3px rgba(26, 23, 21, 0.22));
}

.occasion-tile__label {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 600;
  line-height: 1.2;
}

.occasion-tile__blurb {
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--ink-3);
}

@media (max-width: 64rem) {
  .occasion-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (max-width: 44rem) {
  .occasion-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-md);
  }

  .occasion-tile__blurb {
    display: none;
  }
}

/* ---- Feature band ----------------------------------------------------------
   A large linked panel beside the product's own terms — the shape the
   reference shop uses for its seasonal block, carrying a deadline instead of
   a discount. */

.feature {
  margin-top: var(--space-3xl);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
  gap: var(--space-2xl);
  align-items: center;
}

.feature__panel {
  position: relative;
  display: grid;
  align-content: end;
  gap: var(--space-xs);
  min-height: 18rem;
  padding: var(--space-xl);
  border-radius: var(--radius-lg);
  background: linear-gradient(150deg, var(--ink), var(--ink-2));
  color: var(--paper-3);
  text-decoration: none;
  overflow: hidden;
  transition: transform var(--dur-base) var(--ease-out);
}

.feature__panel:hover,
.feature__panel:focus-visible {
  transform: translateY(-2px);
  color: var(--paper-3);
}

/* The one place the accent appears on this page at any size, which is what
   keeps it meaning "this is the thing to press" — see tokens.css on the ~5%
   rule. On --accent the label is --accent-ink at 5.02:1. */
.feature__ribbon {
  justify-self: start;
  align-self: start;
  position: absolute;
  top: var(--space-lg);
  left: var(--space-lg);
  padding: var(--space-2xs) var(--space-sm);
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.feature__panel-title {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: 600;
  line-height: 1.1;
}

.feature__panel-cta {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--paper-2);
}

.feature__facts {
  margin: var(--space-md) 0 0;
  display: grid;
  gap: var(--space-md);
}

.feature__fact dt {
  font-weight: 600;
  font-size: var(--text-sm);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.feature__fact dd {
  margin: var(--space-3xs) 0 0;
  line-height: 1.55;
  color: var(--ink-2);
  max-width: 52ch;
}

@media (max-width: 60rem) {
  .feature {
    grid-template-columns: 1fr;
    gap: var(--space-xl);
  }

  .feature__panel {
    min-height: 13rem;
  }
}

/* ---- Colour row ------------------------------------------------------------
   The same keychain, ten times, in everything on the shelf. Five across so
   the row reads as a palette rather than as ten unrelated products. */

.colours {
  margin-top: var(--space-3xl);
}

.colour-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--space-md);
}

.colour-card {
  min-width: 0;
}

.colour-card__link {
  display: grid;
  gap: var(--space-xs);
  text-decoration: none;
  color: inherit;
}

/* The swatch itself tints the stage, so the row shows each filament twice
   over: as the plate of the keychain, and as the surface behind it. `color-
   mix` keeps the tint faint enough that white lettering on a white keychain
   still has an edge to sit against. */
.colour-card__art {
  /* A local property, not a design token: index.astro sets it per card from
     the filament's own hex, which is data rather than theme. Declared here
     with an initial value all the same, so the rule below is complete on its
     own — an inline style that failed to render would otherwise void the
     whole `background` declaration and leave the card transparent. It is also
     what lets css-tokens.test.ts see the property as declared rather than
     invented. */
  --swatch: var(--paper-2);

  display: grid;
  place-items: center;
  aspect-ratio: 1 / 1;
  padding: var(--space-sm);
  border-radius: var(--radius-md);
  border: 1px solid var(--line);
  background: color-mix(in srgb, var(--swatch) 16%, var(--paper-2));
  transition: transform var(--dur-fast) var(--ease-out);
}

.colour-card__link:hover .colour-card__art,
.colour-card__link:focus-visible .colour-card__art {
  transform: translateY(-2px);
}

.colour-card__label {
  font-size: var(--text-sm);
  font-weight: 600;
  text-align: center;
  color: var(--ink-2);
}

@media (max-width: 64rem) {
  .colour-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

@media (max-width: 44rem) {
  .colour-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-sm);
  }
}

/* ---- Product grid ----------------------------------------------------------
   The rest of the catalogue, shown only once there is one. Built on the
   occasion tile rather than beside it: both are "a picture, a name, a
   sentence, in a card", and two near-identical card treatments on one page is
   how a shop starts looking assembled rather than designed. The differences
   are real ones — a photograph fills its frame where a drawing of a keychain
   must not, and a product card can carry a caveat an occasion never does. */

.products {
  margin-top: var(--space-3xl);
}

.product-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
  gap: var(--space-lg);
}

.product-card {
  min-width: 0;
}

.product-card__link {
  display: grid;
  gap: var(--space-xs);
  height: 100%;
  align-content: start;
  padding: var(--space-md);
  border-radius: var(--radius-lg);
  background: var(--paper-3);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-sm);
  color: inherit;
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}

.product-card__link:hover,
.product-card__link:focus-visible {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
  border-color: var(--line-strong);
  color: inherit;
}

.product-card__art {
  display: grid;
  place-items: center;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--paper-2);
}

/* `cover` here, unlike the occasion tiles' `contain`: this is a photograph of
   a product in a setting, which crops without losing the subject, where a
   drawing of a keychain cropped in half loses the name. */
.product-card__art img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The stand-in for a product with no photograph yet. Deliberately reads as a
   label rather than as a failed image: a grey box with a broken-image glyph
   is what a customer assumes is a bug. */
.product-card__placeholder {
  padding: var(--space-md);
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: 600;
  line-height: 1.1;
  text-align: center;
  color: var(--ink-3);
}

.product-card__title {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 600;
  line-height: 1.2;
}

.product-card__blurb {
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--ink-3);
}

/* Advertised but not buyable. Inside the link, so it is part of what a screen
   reader announces for the card — hearing the title and blurb and then
   arriving at a disabled buy control is the exact trip this sentence exists
   to prevent. Weight and colour together, never colour alone. */
.product-card__note {
  margin-top: var(--space-2xs);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--danger);
}

/* ---- Deferred 3D showcase --------------------------------------------------
   `DeferredShowcase` (apps/web/src/islands/DeferredShowcase.tsx) shows a
   poster image and a trigger button until clicked, then swaps in the
   `standScene` canvas — the geometry itself, and the `three` chunk it needs,
   are not fetched until that click. This block styles both states; neither
   introduces a colour outside the existing token set. */

.showcase-section {
  margin-top: var(--space-3xl);
}

.showcase-section h2 {
  margin: 0;
  color: var(--ink);
}

.showcase-section__intro {
  margin: 0.25rem 0 1.5rem;
  color: var(--ink-3);
  max-width: 40rem;
}

.deferred-showcase {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  padding: 2rem;
  border-radius: var(--radius-md);
  border: 1px solid var(--line);
  background: var(--paper);
  text-align: center;
}

.deferred-showcase__stage {
  width: 100%;
  max-width: 32rem;
  aspect-ratio: 4 / 3;
  border-radius: calc(var(--radius-md) - 2px);
  overflow: hidden;
  background: var(--paper-3);
}

.deferred-showcase__poster {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.deferred-showcase__canvas {
  display: block;
  width: 100%;
  height: 100%;
  /* The scene handles its own rotate-to-orbit dragging (standScene.ts). */
  touch-action: none;
  cursor: grab;
}

.deferred-showcase__canvas:active {
  cursor: grabbing;
}

.deferred-showcase__fallback {
  margin: 0;
  max-width: 30rem;
  color: var(--ink-3);
}

.deferred-showcase__trigger {
  padding: 0.7rem 1.4rem;
  border-radius: 0.6rem;
  border: none;
  background: var(--accent);
  color: var(--paper-3);
  font-weight: 700;
  font-size: 1rem;
  cursor: pointer;
}

.deferred-showcase__trigger:hover:not(:disabled) {
  background: var(--accent-deep);
}

.deferred-showcase__trigger:disabled {
  cursor: wait;
  opacity: 0.75;
}

/* ---- How it works ---------------------------------------------------------
   A three-step list, deliberately plain: this is reassurance copy, not a
   second call to action. */

.how-it-works {
  margin-top: var(--space-3xl);
}

.how-steps {
  list-style: none;
  margin: 1.5rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 1.5rem;
}

.how-steps__step {
  position: relative;
  padding: 1.5rem;
  border-radius: var(--radius-md);
  background: var(--paper);
  border: 1px solid var(--line);
  overflow: hidden;
}

/* The same repeating hairline as the hero and the name strip, very faint,
   confined to a corner rather than washing the whole card — a step in a
   real, ordered process gets the printer's own texture as a quiet watermark,
   not a loud background. Static: no motion budget was spent here, on
   purpose (see the brief's own restraint note) — the hero and the name
   strip are the page's two moving elements, and a third would dilute both. */
.how-steps__step::before {
  content: '';
  position: absolute;
  inset: -1px -1px auto auto;
  width: 5rem;
  height: 5rem;
  opacity: 0.5;
  background-image: repeating-linear-gradient(
    135deg,
    var(--line) 0,
    var(--line) 1px,
    transparent 1px,
    transparent var(--layer-pitch)
  );
  mask-image: radial-gradient(circle at 100% 0%, #000 0%, transparent 70%);
  pointer-events: none;
}

.how-steps__step h3 {
  position: relative;
  margin: 0.75rem 0 0.5rem;
  font-size: 1.2rem;
  letter-spacing: -0.005em;
}

.how-steps__step p {
  margin: 0;
  color: var(--ink-3);
}

/* These three steps are a real, ordered sequence — design, print, ship, in
   that order, each depending on the last — which is what earns them a
   numbered marker at all (an arbitrary list would not). The rings inside
   each marker are static, not decorative motion: concentric bands at the
   same --layer-pitch as the hero, the printer's own cross-section on the
   one number a customer reads before anything has been printed yet. */
.how-steps__number {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  background: repeating-radial-gradient(
    circle at 50% 50%,
    var(--ink-2) 0,
    var(--ink-2) 2px,
    var(--ink) 2px,
    var(--ink) var(--layer-pitch)
  );
  color: var(--paper-3);
  font-weight: 700;
}

/* ---- Signup band -----------------------------------------------------------
   Two columns: the reason on the left, the field on the right. The reference
   shop centres a single field under a single line of copy, which is the right
   shape when the band is one of twelve on a page and nobody is expected to
   read it. Here it is one of seven, and the sentence explaining what will
   actually arrive is doing most of the work. */

.signup {
  margin-top: var(--space-3xl);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-2xl);
  align-items: center;
  padding: var(--space-2xl);
  border-radius: var(--radius-lg);
  background: var(--paper-2);
}

.signup__lede {
  margin: var(--space-2xs) 0 0;
  color: var(--ink-2);
  max-width: 44ch;
}

.signup__label {
  display: block;
  margin-bottom: var(--space-2xs);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-2);
}

/* `align-items: stretch` so the button is exactly as tall as the field beside
   it. Matching them by hand with a height is what produces the one-pixel
   misalignment that makes a form look homemade. */
.signup__row {
  display: flex;
  align-items: stretch;
  gap: var(--space-xs);
}

.signup__input {
  flex: 1 1 auto;
  /* Without this the input's default intrinsic width (roughly 20 characters)
     acts as a floor on the flex item and pushes the button out of the row on
     a narrow screen. */
  min-width: 0;
}

.signup__submit {
  flex: 0 0 auto;
}

.signup__note {
  margin: var(--space-sm) 0 0;
  font-size: var(--text-sm);
  max-width: none;
}

.signup__note--ok,
.signup__note--already {
  color: var(--ink-2);
}

/* Colour is never the only signal — each message says in words what happened,
   and these two only differ from the others in weight. */
.signup__note--invalid,
.signup__note--error {
  color: var(--danger);
  font-weight: 600;
}

@media (max-width: 60rem) {
  .signup {
    grid-template-columns: 1fr;
    gap: var(--space-lg);
    padding: var(--space-xl) var(--space-lg);
  }
}

@media (max-width: 30rem) {
  /* The button drops below the field rather than squeezing it to nothing.
     Buttons must never carry two lines of text (and "Sign up" would), and a
     40px-wide email field is not a field. */
  .signup__row {
    flex-wrap: wrap;
  }

  .signup__submit {
    flex: 1 0 100%;
  }
}
