/* ============ Fonts (self-hosted, subset woff2) ============ */
@font-face {
  font-family: "Cabinet Grotesk";
  src: url("/fonts/cabinet-grotesk-700.woff2") format("woff2");
  font-weight: 700;
  font-display: swap;
}
@font-face {
  font-family: "Cabinet Grotesk";
  src: url("/fonts/cabinet-grotesk-800.woff2") format("woff2");
  font-weight: 800;
  font-display: swap;
}
@font-face {
  font-family: "General Sans";
  src: url("/fonts/general-sans-400.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "General Sans";
  src: url("/fonts/general-sans-500.woff2") format("woff2");
  font-weight: 500;
  font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/fonts/jetbrains-mono-400.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}

/* ============ Tokens ============ */
:root {
  --norr-black: #0c1110;
  --fog: #a9b0ac;
  --birch: #eeede8;
  --snow: #fafaf7;
  --moss: #55654f;
  --gold: #a66f25;
  --ink: #1a1d1c;

  /* Live theme vars — animated by ScrollTrigger during the dawn transition */
  --c-bg: var(--norr-black);
  --c-text: var(--birch);
  --c-muted: var(--fog);
  --grain-o: 0.045;

  --font-display: "Cabinet Grotesk", "Helvetica Neue", sans-serif;
  --font-body: "General Sans", "Helvetica Neue", sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, monospace;

  /* 1720, raised from 1440 on 2026-08-12. The shop's card rows had grown to
     near-viewport width while every heading stayed in the 1440 container, so
     the two drifted apart as the window widened: 12px apart at 1440, 230px
     at 1900 — enough that a shelf heading no longer shared an edge with the
     row it labelled. One measure for everything fixes that by construction.
     The explore carousel is the deliberate exception and still bleeds. */
  --container: 1720px;
  --gutter: clamp(1.25rem, 4vw, 3rem);
  /* One vertical rhythm for section top/bottom padding — every section-to-
     section gap is 2× this, so the page keeps a consistent beat. */
  /* ---- One spacing rule for the whole site (Alex, 2026-08-13) ----
     A section owns the space ABOVE it and none below, so a boundary between
     any two sections is exactly ONE --section-y.

     It used to be paid on both edges, which made section-to-section double
     what hero-to-section was: 172px against 68px on the homepage. Alex spotted
     it by eye. Measured across every page the spread ran 0px to 371px, because
     the story and cabins sections also carry their own larger values.

     Two exceptions, and no others:
     - a section that paints its own background (.voucher, .wearing) keeps
       symmetric padding, or its content sits on the colour edge. Their
       boundaries are deliberate hard cuts anyway.
     - the last section on a page keeps its bottom padding so the content does
       not sit against the footer. That is .newsletter almost everywhere. */
  --section-y: clamp(4.5rem, 9vh, 7rem);
}
html[data-theme-start="light"] {
  --c-bg: var(--birch);
  --c-text: var(--ink);
  --c-muted: #6b716d;
  --grain-o: 0;
}

/* ============ Base ============ */
*,
*::before,
*::after {
  box-sizing: border-box;
}
html {
  -webkit-text-size-adjust: 100%;
}
body {
  margin: 0;
  background: var(--c-bg);
  color: var(--c-text);
  font-family: var(--font-body);
  font-weight: 400;
  line-height: 1.6;
  font-size: 1rem;
  overflow-x: clip;
}
img,
video,
picture {
  max-width: 100%;
  display: block;
}
img {
  height: auto; /* keep aspect ratio when width/height attrs are constrained */
}
a {
  color: inherit;
  text-decoration: none;
}
h1,
h2,
h3 {
  font-family: var(--font-display);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.02;
  margin: 0 0 0.5em;
}
p {
  margin: 0 0 1em;
}
.container {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* Skip to content. Parked off-screen rather than display:none, because a
   hidden element cannot take focus and the link would never appear. z-index
   clears the nav (100) and the transition veil, which both sit above the
   page; without it the link takes focus while covered and the tab order
   looks broken instead of helpful. */
.skip-link {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 9999;
  transform: translateY(-120%);
  padding: 0.75rem 1.25rem;
  background: var(--ink);
  color: var(--birch);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  border-bottom-right-radius: 4px;
}
.skip-link:focus-visible {
  transform: translateY(0);
  outline: 2px solid var(--birch);
  outline-offset: -6px;
}

/* No-JS fallback: give sections their own backgrounds so pages stay readable */
html:not(.js) [data-theme="light"] {
  background: var(--birch);
  color: var(--ink);
}
html:not(.js) [data-theme="dark"] {
  background: var(--norr-black);
  color: var(--birch);
}

/* ============ Utility type ============ */
.eyebrow {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--c-muted);
  margin-bottom: 1.5rem;
}
.section-title {
  font-size: clamp(2.2rem, 5vw, 4.6rem);
}

/* Masked text animation shells */
.word-mask,
.line-mask {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
}
.word,
.line {
  display: inline-block;
  will-change: transform;
}
html.js:not(.reduced) .word,
html.js:not(.reduced) .line {
  transform: translateY(115%);
}

/* Image reveal shells */
html.js:not(.reduced) .reveal {
  clip-path: inset(100% 0 0 0);
}
html.js:not(.reduced) .reveal img {
  transform: scale(1.08);
}
html.reduced .reveal,
html.reduced .word,
html.reduced .line {
  opacity: 1;
  transform: none;
  clip-path: none;
}

/* ============ Grain (dark sections only, via --grain-o) ============ */
.grain {
  position: fixed;
  inset: -50%;
  width: 200%;
  height: 200%;
  pointer-events: none;
  z-index: 60;
  opacity: var(--grain-o);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E");
}

/* ============ Page transition veil ============ */
.transition-veil {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: var(--norr-black);
  transform: translateY(101%);
  display: grid;
  place-items: center;
  pointer-events: none;
}
/* Arriving mid-transition (class set pre-paint in the <head>): veil covers
   from the first frame — no flash of the new page before main.js reveals it.
   Failsafe: if scripts never run, lift the veil after 3s so it can't stick. */
html.nw-veil-hold .transition-veil {
  transform: translateY(0);
  animation: veil-failsafe 0.6s ease 3s forwards;
}
html.nw-veil-hold .transition-veil__mark {
  opacity: 1;
}
@keyframes veil-failsafe {
  to { transform: translateY(-101%); }
}
.transition-veil__mark {
  font-family: var(--font-display);
  font-weight: 800;
  letter-spacing: 0.35em;
  font-size: 0.95rem;
  color: var(--fog);
  opacity: 0;
}

/* ============ Nav ============ */
.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  padding-block: 1.1rem;
  /* Solid backdrop is toggled on scroll; var(--c-bg) is animated by the
     dawn transition, so the bar always matches the page behind it.
     No backdrop-filter — it breaks on iOS Safari fixed headers. */
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background 0.3s ease, border-color 0.3s ease;
}
.nav--solid {
  background: var(--c-bg);
  border-bottom-color: color-mix(in srgb, var(--c-muted) 22%, transparent);
}
html.js .nav {
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
html.js .nav.nav--visible {
  opacity: 1;
  transform: none;
}
.nav__inner {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  align-items: center;
  gap: 1.4rem;
}
.nav__mark {
  font-family: var(--font-display);
  font-weight: 800;
  letter-spacing: 0.28em;
  font-size: 0.95rem;
  margin-right: auto; /* push everything else to the right */
}
.nav__links {
  display: flex;
  align-items: center;
  gap: 2rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
.nav__item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.3rem;
}
.nav__links a {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.nav__item > a:hover {
  color: var(--moss);
}
[data-theme-start="dark"] .nav__item > a:hover {
  color: var(--c-muted);
}

/* Submenu trigger chevron */
.nav__chev {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem;
  background: none;
  border: 0;
  color: var(--c-muted);
  cursor: pointer;
}
.nav__chev svg { transition: transform 0.25s ease; }
.nav__item--sub.is-open .nav__chev svg { transform: rotate(180deg); }

/* Desktop dropdown */
.nav__sub {
  position: absolute;
  top: 100%;
  left: -0.6rem;
  min-width: 12rem;
  display: flex;
  flex-direction: column;
  padding: 0.5rem 0;
  margin-top: 0.35rem;
  background: var(--c-bg);
  border: 1px solid color-mix(in srgb, var(--c-muted) 24%, transparent);
  box-shadow: 0 24px 50px -30px rgba(12, 17, 16, 0.55);
  opacity: 0;
  visibility: hidden;
  transform: translateY(6px);
  transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s;
}
/* Invisible bridge so the mouse can travel from label to panel */
.nav__item--sub::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: 0.6rem;
}
.nav__item--sub.is-open > .nav__sub {
  opacity: 1;
  visibility: visible;
  transform: none;
}
.nav__sub a {
  padding: 0.6rem 1.1rem;
  color: var(--c-muted);
  white-space: nowrap;
}
.nav__sub a:hover { color: var(--c-text); }

.nav__cart {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.nav__cart:hover { color: var(--moss); }
[data-theme-start="dark"] .nav__cart:hover { color: var(--c-muted); }

.nav__lang {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  /* A hairline divider sets the switcher apart from the page links */
  border-left: 1px solid color-mix(in srgb, var(--c-muted) 40%, transparent);
  padding-left: 1.2rem;
}
.nav__lang a { color: var(--c-muted); }
.nav__lang a[aria-current="true"] { color: var(--c-text); }
.nav__lang-sep {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  color: color-mix(in srgb, var(--c-muted) 55%, transparent);
}

/* Burger + drawer-only language row: hidden on desktop */
.nav__burger { display: none; }
.nav__item--drawerlang { display: none; }

/* ---------- Mobile: burger + drawer ---------- */
@media (max-width: 820px) {
  /* 44×44 tap area (WCAG 2.5.8) carved OUT of the bar, not added to it.
     Measured 2026-08-14: the button was 34.8×26.8, and .nav__inner measured
     26.8 too — the burger IS the tallest thing in the bar, so it sets the
     header's height on mobile. Padding it to 44 the obvious way would have
     grown the fixed header from 63px to 80px on every page of the site.
     Padding takes it to 44.4×44.4; the negative margins hand back exactly the
     growth ((44.4-26.8)/2 = 8.8px block, (44.4-34.8)/2 = 4.8px inline), so the
     layout box stays 34.8×26.8, the bar stays 63px, the icon does not move.
     44.4 < 63, so the enlarged target still sits wholly inside the bar and
     cannot steal a tap from the page underneath. */
  .nav__burger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.95rem 0.7rem;
    margin-block: -0.55rem;
    margin-right: -0.3rem;
    background: none;
    border: 0;
    color: var(--c-text);
    cursor: pointer;
  }
  .nav__burger-box {
    position: relative;
    display: block;
    width: 22px;
    height: 14px;
  }
  .nav__burger-box span {
    position: absolute;
    left: 0;
    right: 0;
    height: 1.5px;
    background: currentColor;
    transition: transform 0.3s ease, opacity 0.3s ease;
  }
  .nav__burger-box span:nth-child(1) { top: 2px; }
  .nav__burger-box span:nth-child(2) { bottom: 2px; }
  .nav.is-open .nav__burger-box span:nth-child(1) { transform: translateY(4.25px) rotate(45deg); }
  .nav.is-open .nav__burger-box span:nth-child(2) { transform: translateY(-4.25px) rotate(-45deg); }

  .nav__lang--bar { display: none; }

  /* The link list becomes a full-width drawer under the bar */
  .nav__links {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 0.25rem var(--gutter) 1.4rem;
    background: var(--c-bg);
    border-bottom: 1px solid color-mix(in srgb, var(--c-muted) 22%, transparent);
    box-shadow: 0 30px 50px -30px rgba(12, 17, 16, 0.5);
    /* collapsed */
    display: none;
  }
  .nav.is-open .nav__links { display: flex; }

  .nav__item {
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 0;
    padding: 0.95rem 0;
    border-bottom: 1px solid color-mix(in srgb, var(--c-muted) 16%, transparent);
  }
  .nav__item > a { font-size: 0.9rem; }
  .nav__chev {
    padding: 0.4rem;
    margin: -0.4rem 0;
    color: var(--c-muted);
    position: relative;
  }
  /* The chevron draws at 22.8×19.8 and must keep drawing at 22.8×19.8: it sits
     in a flex row with the link and already carries a negative margin to stop
     it inflating the row. So the tap area is an invisible overlay instead of
     padding — a pseudo-element is out of flow, so this cannot move anything.
     The drawer row measures 54.4px, so 44 fits inside it and never reaches the
     row above or below. The chevron sits at the far right of a 335px
     space-between row, so the ~11px it overhangs on the left comes nowhere
     near the link's own tap area. Mobile only: on desktop these are hover
     dropdowns and an overlay this size would reach the neighbouring item. */
  .nav__chev::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
  }

  /* Accordion, not dropdown */
  .nav__sub {
    position: static;
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: none;
    min-width: 0;
    flex-basis: 100%;
    background: none;
    border: 0;
    box-shadow: none;
    margin: 0;
    padding: 0;
    display: none;
  }
  .nav__item--sub.is-open > .nav__sub { display: flex; padding-top: 0.4rem; }
  .nav__item--sub::after { content: none; }
  .nav__sub a {
    padding: 0.55rem 0 0.55rem 0.9rem;
    font-size: 0.82rem;
    text-transform: none;
    letter-spacing: 0;
  }

  /* Language row inside the drawer */
  .nav__item--drawerlang {
    display: flex;
    align-items: center;
    justify-content: space-between;
    border-bottom: 0;
  }
  .nav__drawerlang-label {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--c-muted);
  }
  .nav__drawerlang-opts { display: flex; gap: 1.1rem; }
  .nav__drawerlang-opts a { color: var(--c-muted); }
  .nav__drawerlang-opts a[aria-current="true"] { color: var(--c-text); }
}

/* ============ Buttons & links ============ */
.btn {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: 1rem 2rem;
  border: none;
  cursor: pointer;
  transition: background 0.3s ease, color 0.3s ease;
}
.btn--light {
  background: var(--birch);
  color: var(--ink);
}
.btn--light:hover {
  background: var(--snow);
}
.btn--moss {
  background: var(--moss);
  color: var(--snow);
}
.btn--moss:hover {
  background: #46543f;
}
.link-arrow {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--moss);
  border-bottom: 1px solid currentColor;
  padding-bottom: 0.2em;
}
.link-arrow--light {
  color: var(--c-muted);
}

/* ============ Hero ============ */
.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: flex-end;
  overflow: clip;
}
.hero__media {
  position: absolute;
  inset: 0;
}
.hero__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  will-change: transform;
}
/* Hero still — one cinematic zoom-out that settles and stops, rather than a
   loop. Starts 12% in and eases back to true size: quick off the mark, then
   slowing to a halt. Runs once (`both`), so the image is simply at rest for
   the rest of the visit — a permanent slow pan is the thing that reads cheap.
   Quart-out at 6s, not expo at 4.5s: expo put 97% of the travel in the first
   1.2s, so most of it was imperceptible and the move felt like a jolt rather
   than a settle. Measured, not guessed. 6s rather than 5 (Alex, 2026-08-12)
   stretches the visible part to roughly three and a half seconds.
   Shared with the shop hero, so both pages settle at the same rate.
   Pure CSS on purpose: this replaced an autoplay video, so it must not
   depend on JS or a network fetch to look right. */
.hero__img,
.hero__img :is(img, picture) {
  width: 100%;
  height: 100%;
  display: block;
}
.hero__img :is(img, picture) {
  object-fit: cover;
  /* The subject sits right of centre. On desktop the frame is wider than the
     16:9 source, so it crops vertically and this has no effect; on a portrait
     phone it crops hard horizontally, and dead-centre cut the second figure
     and the cabin out of shot. 62% keeps both people, the fire and the
     A-frame in frame. */
  object-position: 62% 50%;
}
.hero__img {
  transform-origin: 50% 55%;
  will-change: transform;
  animation: hero-settle 6s cubic-bezier(0.25, 1, 0.5, 1) both;
}
@keyframes hero-settle {
  from {
    transform: scale(1.12);
  }
  to {
    transform: scale(1);
  }
}
@media (prefers-reduced-motion: reduce) {
  .hero__img {
    animation: none;
    transform: none;
  }
}
.hero__shade {
  position: absolute;
  inset: 0;
  background: linear-gradient(
      to top,
      rgba(12, 17, 16, 0.88) 0%,
      rgba(12, 17, 16, 0.25) 45%,
      rgba(12, 17, 16, 0.35) 100%
    );
}
.hero__content {
  position: relative;
  z-index: 2;
  padding-bottom: clamp(3rem, 8vh, 6rem);
  width: 100%;
}
/* Capped at 7rem, down from 10.5rem (2026-08-12). At 10.5 the headline was
   168px on a wide screen and the photograph became a backdrop behind type —
   which matters more now the hero is a still, since the picture is doing the
   work the video used to. The shorter line helps as much as the smaller size:
   five words on one line instead of eight over two. */
.hero__title {
  font-size: clamp(2.8rem, 7vw, 7rem);
  letter-spacing: -0.035em;
  color: var(--birch);
  margin-bottom: 0.3em;
}
.hero__sub {
  max-width: 34ch;
  margin: 0 0 2rem;
  color: var(--birch);
  font-size: clamp(1rem, 1.5vw, 1.25rem);
  line-height: 1.5;
  text-wrap: balance;
}
.hero__eyebrow {
  color: var(--fog);
}
/* The vertical "Scroll" cue in the hero's bottom-right is gone (2026-08-12).
   Nobody needs telling that a page scrolls, and the tic is common enough in
   generated templates that it reads as one. */

/* ============ Sections ============ */
section {
  position: relative;
}
.featured,
.collection,
.shop {
  padding-top: clamp(3rem, 7vh, 5rem);
  padding-bottom: clamp(5rem, 14vh, 10rem);
}
/* Homepage collections: symmetric standard rhythm (the shared rule above
   keeps its asymmetric shop-page spacing for .featured / .shop). */
.collection {
  padding-block: var(--section-y) 0;
}
.featured .section-title,
.collection .section-title {
  margin-bottom: 3rem;
}
/* The bench follows the collections in the same light zone — a small top
   pad keeps the two reading as one shop story without gluing them. */
.collection--bench {
  padding-top: clamp(1rem, 3vh, 2rem);
}
.collection--bench .section-title {
  font-size: clamp(1.8rem, 3.6vw, 3.2rem);
  margin-bottom: 2.2rem;
}
.featured__more {
  margin-top: 3rem;
}

/* ============ Home FAQ ============
   Four objections answered above the newsletter. Native <details>, so it
   works with JS off and the answers stay in the DOM for search engines.
   The rows are capped at a reading measure but the rules run the full
   container width, so the section keeps the page's one left edge. */
.homefaq {
  padding-block: var(--section-y) 0;
}
.homefaq__list {
  margin-top: 2.4rem;
  border-top: 1px solid color-mix(in srgb, var(--c-muted) 34%, transparent);
}
.homefaq__item {
  border-bottom: 1px solid color-mix(in srgb, var(--c-muted) 34%, transparent);
}
.homefaq__q {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 1.35rem 0;
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 1.6vw, 1.3rem);
  line-height: 1.3;
  cursor: pointer;
  list-style: none;
  transition: color 0.2s ease;
}
.homefaq__q::-webkit-details-marker {
  display: none;
}
/* Own marker: a plus that becomes a minus when the row is open. */
.homefaq__q::after {
  content: "+";
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: 1.1rem;
  line-height: 1;
  color: var(--c-muted);
  transition: transform 0.25s ease, color 0.2s ease;
}
.homefaq__item[open] .homefaq__q::after {
  content: "–";
  color: var(--c-text);
}
.homefaq__q:hover,
.homefaq__item[open] .homefaq__q {
  color: var(--moss-light, var(--c-text));
}
.homefaq__a {
  max-width: 62ch;
  margin: 0 0 1.5rem;
  color: var(--c-muted);
  line-height: 1.65;
}
.homefaq__size-layout {
  display: grid;
  grid-template-columns: minmax(0, 62ch) minmax(360px, 1fr);
  gap: clamp(2.5rem, 6vw, 6rem);
  align-items: start;
  padding-bottom: 1.5rem;
}
.homefaq__size-copy .homefaq__a:last-child { margin-bottom: 0; }
.homefaq__size-layout .measurement-guide { margin-top: 0.2rem; }
.homefaq .link-arrow {
  display: inline-block;
  margin-top: 2.2rem;
}
@media (max-width: 900px) {
  .homefaq__size-layout {
    grid-template-columns: 1fr;
    gap: 1.4rem;
  }
}
@media (prefers-reduced-motion: reduce) {
  .homefaq__q,
  .homefaq__q::after {
    transition: none;
  }
}

/* ============ Voucher banner ============
   A slab of near-black between two light sections. No dawn transition and no
   gradient at its edges on purpose: the hard cut is the effect.

   Copy left, garments right, form beneath the copy, at every width. The image
   is a foreground element in a grid column rather than a full-bleed backdrop,
   which is what fixes the softness: as a backdrop it was stretched across the
   whole viewport, and the subject inside the file is only ~820px wide, so it
   was upscaled roughly 3x on a retina screen. In a column it renders around
   a third of that. `contain` so the garments are never cropped; the file's
   ground is the same #0C1110 as the section, so the letterbox is invisible. */
.voucher {
  background: var(--norr-black);
  padding-block: clamp(3rem, 7vh, 5rem);
}
.voucher__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.8fr);
  grid-template-areas:
    "copy media"
    "act  media";
  align-items: center;
  column-gap: clamp(1.5rem, 4vw, 4rem);
  row-gap: 1.6rem;
}
.voucher__copy { grid-area: copy; align-self: end; }
.voucher__media { grid-area: media; }
.voucher__act { grid-area: act; align-self: start; }

.voucher__media :is(img, picture) {
  display: block;
  width: 100%;
  height: auto;
  object-fit: contain;
}
.voucher__title {
  font-family: var(--font-display);
  font-size: clamp(1.7rem, 3.4vw, 3rem);
  line-height: 1.02;
  letter-spacing: -0.02em;
  margin: 0 0 0.8rem;
  color: var(--birch);
}
.voucher__lede {
  color: var(--fog);
  font-size: clamp(0.9rem, 1.2vw, 1.05rem);
  line-height: 1.6;
  max-width: 42ch;
  margin: 0;
}
.voucher__lede strong {
  color: var(--birch);
  font-weight: 600;
}
/* Doubled selector on purpose: .newsletter__form sets margin-top: 2rem and is
   defined later in this file, so a single .voucher__form lost the tie and
   silently added 32px above the email box. */
.voucher__form.newsletter__form {
  margin-top: 0;
  max-width: 27rem;
}
/* "Get 15% off" broke across two lines in the narrow mobile button. */
.voucher__form .btn {
  white-space: nowrap;
}
.voucher__note {
  margin-top: 0.9rem;
}
@media (max-width: 700px) {
  /* Stacked, not side by side. Sharing a 390px row with the copy left the
     media column 140px, which put each garment at ~64px — too small to read a
     print at any file size, which is why it looked blurred however large the
     source got. Full width takes the tee to ~161px. The garments lead because
     they are the hook; the form still spans the full width or the input
     collapses to a few characters. */
  .voucher__inner {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "media"
      "copy"
      "act";
    row-gap: 1.8rem;
  }
  .voucher__copy,
  .voucher__act { align-self: start; }
  .voucher__media {
    max-width: 22rem;
    margin-inline: auto;
  }
  .voucher__form.newsletter__form { max-width: none; }
  /* Even rhythm rather than the 59 / 29 / 13 / 61 / 59 it had been: the band
     above the garments and the one under the email box now match the gaps
     between the blocks, so nothing reads as a stray pocket of black. Title and
     lede stay tighter than the rest because they are one unit. */
  .voucher {
    padding-block: 2rem;
  }
  .voucher__inner {
    row-gap: 1.5rem;
  }
}

/* Breather — a quiet text-only pause between the image-heavy sections.
   Centered and calm, in the body face — deliberately NOT the bold, tight,
   left-aligned display headlines used everywhere else, so it reads as a
   pause, not another section header. No serif. */
.breather {
  padding-block: var(--section-y);
  text-align: center;
}
.breather .eyebrow {
  margin-bottom: 2rem;
}
.breather__statement {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: clamp(1.5rem, 3.2vw, 2.5rem);
  line-height: 1.34;
  letter-spacing: -0.005em;
  max-width: 22ch;
  margin: 0 auto;
}
.breather__copy {
  color: var(--c-muted);
  max-width: 44ch;
  margin: 1.8rem auto 0;
  font-size: 1rem;
  line-height: 1.65;
}
/* .breather no longer runs on the English homepage: .wearing below replaced it
   (2026-08-13). Kept because /de/ and /fi/ still render it. */

/* ============ What you're wearing ============
   Photograph flush to the left viewport edge, the spec table to the right of a
   hairline rule. Full-bleed by the same technique .band__inner and .uspbar
   use, so the image can reach the edge while the text keeps a reading gutter.

   It deliberately rhymes with .band's photo-left shape, but sits three
   sections above it with the full-height dark voucher between, so the two can
   never share a screen.

   The rule is a plain border, not a divider element: an earlier draft carried
   a rotated "FINLAND / 62°N" label inside it, which next to a paragraph about
   how a garment is made reads as a manufacturing origin. The blanks are
   Stanley/Stella and the printing is Printful, so the claim could not be made. */
/* Paints its own palette rather than riding the root, the same reason the
   voucher banner does. The band below is a dark [data-theme] section, so its
   palette scrub runs from "band top at viewport bottom" to "band top at 25%",
   about 75vh of travel, and everything above it greys while that happens.
   This section is 627px against 713px of travel, so it was on screen for the
   whole scrub and washed to a flat mid-grey: measured rgb(135,137,134), which
   is 45% of the way from birch to near-black. The breather it replaced hid
   this because centred grey text on grey barely reads as wrong.

   Setting the three palette vars locally means every child (title, copy, spec
   labels, link) stays correct in one step, and the root keeps scrubbing for
   the band's benefit. */
.wearing {
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  display: grid;
  /* The photograph is 4:5. The column and the row height are sized to land the
     panel near that ratio so object-fit: cover has almost nothing to take.
     At 46fr and 66vh the panel came out 696x627, i.e. LANDSCAPE against a
     portrait source, and cover ate 28% of the picture's height. Narrower and
     taller shows the scene instead of a crop of it. (Alex, 2026-08-13) */
  grid-template-columns: minmax(0, 40fr) minmax(0, 60fr);
  align-items: stretch;
  background: var(--birch);
  --c-text: var(--ink);
  --c-muted: #6b716d;
  color: var(--c-text);
}
/* aspect-ratio, not a min-height in vh: the column width comes from the grid,
   so 4:5 resolves to the exact height the photograph wants and the grid row
   takes it. A vh-based height only matched the ratio at one viewport size and
   drifted everywhere else (11% cut at 1920). The floor is a safety net for
   narrow two-column widths, where the text column would otherwise set a taller
   row and cover would start cropping again. */
/* A padding ratio box, NOT aspect-ratio. Verified in WebKit: as a stretched
   grid item, aspect-ratio gave a 0x0 panel on a phone and the photograph
   simply did not exist, while Chromium rendered it 390x488. The stretch
   resolves the cross size to a definite 0 before the ratio is consulted, and
   in a single-column grid there is nothing else to give the row height, so it
   collapses onto itself. Desktop hid it because the text column set the row.

   ::before has real in-flow height, 125% of the column width, so the row is
   sized before any stretching happens. Every engine agrees. If the text column
   ever grows taller than the photograph, the panel simply stretches and cover
   trims a little, which is the right failure. Do not "simplify" this back to
   aspect-ratio without testing WebKit. */
.wearing__media {
  position: relative;
  overflow: hidden;
}
.wearing__media::before {
  content: "";
  display: block;
  padding-top: 125%;
}
.wearing__media :is(picture, img) {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.wearing__body {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: clamp(2.5rem, 7vh, 4.5rem);
  padding-inline: clamp(1.6rem, 4vw, 3.5rem) var(--gutter);
  border-left: 1px solid color-mix(in srgb, var(--c-muted) 26%, transparent);
}
.wearing__title {
  font-size: clamp(1.85rem, 3vw, 2.9rem);
  margin: 0 0 1.1rem;
}
/* .line-mask is inline-block site-wide, so the two written lines reflow onto
   one as soon as the column is wide enough. Widening the text column to 60fr
   for the taller photograph did exactly that. Blocking the masks here keeps
   the break where it was written, without touching the shared rule. */
.wearing__title .line-mask {
  display: block;
}
.wearing__copy {
  color: var(--c-muted);
  max-width: 44ch;
  line-height: 1.65;
  margin: 0 0 2rem;
}
.wearing__spec {
  margin: 0 0 2.2rem;
  max-width: 34rem;
}
.wearing__row {
  display: grid;
  grid-template-columns: minmax(0, 7.5rem) minmax(0, 1fr);
  gap: 1rem;
  align-items: baseline;
  padding: 0.85rem 0;
  border-top: 1px solid color-mix(in srgb, var(--c-muted) 24%, transparent);
}
.wearing__row:last-child {
  border-bottom: 1px solid color-mix(in srgb, var(--c-muted) 24%, transparent);
}
.wearing__row dt {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--moss);
}
.wearing__row dd {
  margin: 0;
}
.wearing__cta {
  align-self: start;
}
@media (max-width: 860px) {
  .wearing {
    grid-template-columns: 1fr;
  }
  /* Nothing to override here any more. Stacked full width, the image is in
     flow and carries its own 4:5 height, so no vh floor is needed and none
     should be added: a floor fought the ratio and cropped narrow handsets. */
  .wearing__body {
    border-left: 0;
    padding-inline: var(--gutter);
    padding-block: clamp(2rem, 6vh, 3rem) clamp(2.6rem, 8vh, 4rem);
  }
}

/* Product grids — asymmetric rhythm, not centered-everything */
.grid {
  display: grid;
  gap: 2.5rem var(--gutter);
}
/* =========================================================
   Wide card grid. Born 2026-08-12 as a true full-bleed
   primitive copied from the explore strip; it no longer
   bleeds. Breaking out to 100vw while headings stayed in the
   container gave the page two left edges that drifted apart
   as the window widened (12px at 1440, 230px at 1900), and a
   shelf heading that didn't line up with its own row reads as
   broken. It now sits on the container's measure, so cards
   and the text above them share one edge at every width.
   Cards keep the tight inter-card gap, so a row still reads
   as one band rather than as cards floating apart.
   The class name is historical — the explore carousel
   (.explore__grid) is now the only thing that bleeds.
   ========================================================= */
.bleed-grid {
  --bleed-gap: clamp(0.5rem, 0.9vw, 1rem);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* Row gap is the FAR half of the caption's proximity: it has to beat
     .card__meta's padding-top by enough that a caption reads as belonging to
     the image above it, not the one below. Aiming for roughly 3:1. */
  gap: clamp(3.25rem, 6vh, 4.5rem) var(--bleed-gap);
}
.bleed-grid--2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.bleed-grid--4 {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}
@media (max-width: 900px) {
  .bleed-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /* 1.5rem gave a 24px row gap against a 10px image-to-caption gap, so a
       caption was only 2.4x nearer the image it names. On a 2-up phone grid
       that is the difference between reading the pairing instantly and having
       to work it out. 2.75rem takes it to 44px, i.e. above 4x. */
    gap: 2.75rem var(--bleed-gap);
  }
  .bleed-grid--2 {
    grid-template-columns: 1fr;
  }
  .bleed-grid--4 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
.grid--featured {
  grid-template-columns: repeat(4, 1fr);
}
/* Shop shelves: uniform rows — three equal cards per row on desktop,
   two on small screens. No hero spans; every design gets the same stage. */
.grid--shop {
  grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 900px) {
  .grid--featured,
  .grid--shop {
    grid-template-columns: repeat(2, 1fr);
    /* Tighter gutters on mobile so the two cards run larger and fill the
       row — less dead space between and under them. */
    gap: 1.5rem 0.7rem;
  }
}

/* ============ Product card ============ */
.card {
  display: block;
}
.card__media {
  position: relative;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--snow);
}
.card__img {
  position: absolute;
  inset: 0;
  transition: opacity 0.4s ease;
}
.card__img img,
.card__img picture {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.card__img--back {
  opacity: 0;
}
/* Product mockups are square studio renders on a pure white ground, so
   `cover` in the 4:5 frame sliced 12.5% off each side — sleeves ran off the
   card edge — and blew the garment up to ~640 CSS px on a wide screen, which
   read as shouting rather than showing. Match the frame to the source ratio,
   `contain` so nothing is cropped, and inset it so the garment stands in its
   own air. The white background is the mockups' own ground (#fff at every
   corner): without it the inset reads as a visible box-in-box seam.
   Cabin and collection cards keep `cover` — those are photographs. */
/* No aspect-ratio here: product cards inherit 4:5 from .card__media, the same
   box collection cards already used. Every garment mockup was recut to 4:5 on
   2026-08-13, so they fill it edge to edge.

   Uniform on purpose. Alex asked that a tee sitting next to a hoodie carry its
   title at the same height, and one shared box aspect is what guarantees that
   in every grid, including the mixed favourites rows and the related strip on
   a product page.

   Mugs, totes and beanies have no shot mockup and use square Shopify photos.
   They letterbox here, which costs them nothing: the product renders exactly
   the size it did in a square box, on the same white ground as the bands
   above and below it, and their rows still line up with everything else. */
.card--product .card__media {
  background: #fff;
}
/* Cards whose image is a shot mockup rendered on the page's own #EEEDE8.
   Frame goes transparent and the inset goes away: the garment then sits
   directly on the page with no panel and no edge, which is the whole reason
   the mockups were rendered on that colour. Everything still on a Printful
   white ground keeps the white frame above — mixing the two is what produced
   the visible box-in-box seam. */
.card--onbg .card__media {
  background: transparent;
}
.card--onbg .card__img {
  padding: 0;
}
/* The worn shot behind the mockup. It is a photograph, so it fills the square
   edge to edge rather than floating like the garment does — hovering turns the
   card from a shirt on the page's own colour into a full picture, which is the
   point of it. `cover` and no inset, overriding both the `contain` that keeps
   mockups uncropped and the inset used on white-ground cards.
   Both selectors name the class twice on purpose: the `contain` and inset
   rules for .card--product .card__img are defined further down this file with
   the same specificity, so matching them would lose on source order. */
.card--product .card__img--back.card__img--life {
  padding: 0;
}
.card--product .card__img--back.card__img--life :is(img, picture) {
  object-fit: cover;
}
.card--product .card__img {
  padding: 5%;
}
.card--product .card__img :is(img, picture) {
  object-fit: contain;
}
/* Tap-to-flip (touch) — reveal the other print. */
.card.is-flipped .card__img--back {
  opacity: 1;
}
.card.is-flipped .card__img--front {
  opacity: 0;
}
.card__flip {
  position: absolute;
  top: 0.6rem;
  left: 0.6rem;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 0.34rem;
  padding: 0.3rem 0.55rem 0.3rem 0.5rem;
  background: color-mix(in srgb, var(--snow) 84%, transparent);
  color: var(--ink);
  border: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
  border-radius: 100px;
  font-family: var(--font-mono);
  font-size: 0.58rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1;
  backdrop-filter: blur(4px);
  cursor: pointer;
  opacity: 0.9;
  transition: opacity 0.25s ease;
}
.card__flip:hover,
.card:hover .card__flip {
  opacity: 1;
}
/* Quiet "view" cue — hover-only, styled like the flip pill. */
.card__view {
  position: absolute;
  right: 0.6rem;
  bottom: 0.6rem;
  z-index: 2;
  display: none;
  padding: 0.3rem 0.55rem;
  background: color-mix(in srgb, var(--snow) 84%, transparent);
  color: var(--ink);
  border: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
  border-radius: 100px;
  font-family: var(--font-mono);
  font-size: 0.58rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1;
  backdrop-filter: blur(4px);
}
@media (hover: hover) and (pointer: fine) {
  .card__view {
    display: inline-flex;
    opacity: 0;
    transform: translateY(4px);
    transition: opacity 0.25s ease, transform 0.25s ease;
  }
  .card:hover .card__view {
    opacity: 1;
    transform: translateY(0);
  }
}
@media (hover: hover) and (pointer: fine) {
  .card__img {
    transition: opacity 0.4s ease, transform 0.6s ease;
  }
  .card:hover .card__img--back {
    opacity: 1;
  }
  .card:hover .card__img--front {
    opacity: 0;
  }
  .card:hover .card__img {
    transform: scale(1.03);
  }
  /* Collection cards have no back image — keep the front visible instead
     of fading to a blank white square. */
  .card.card--collection:hover .card__img--front {
    opacity: 1;
  }
}
/* Proximity: the caption has to sit nearer the image it names than the image
   below it, or the eye cannot pair them. Measured at 390px it was 23px above
   and 24px below, i.e. exactly ambiguous, which is what Alex saw. This is the
   near half; the far half is the row gap on .bleed-grid. */
.card__meta {
  padding-top: 0.6rem;
}
/* The price follows its title immediately, always. It is NOT pinned to the
   foot of the card and must not be: a one-line title next to a two-line one
   should close up, not hold an empty line to keep the two prices level
   (Alex, 2026-08-13, having rejected both earlier attempts at levelling them).

   So the caption is plain flow. Two goes at levelling prices across a row have
   now been reverted: a min-height on every title, and pinning the price to the
   bottom of a stretched card. Both bought alignment with a gap, and the gap is
   the thing that reads as broken. */
/* Centred under the garment, which is itself centred in its white frame.
   Left-aligned, the first card's code and title sat within ~14px of the
   viewport edge while every heading on the page starts at the 1440
   container — two different left edges for text, and the mismatch is what
   read as cramped. Centring takes the card's label out of that comparison
   entirely. Cabin and collection cards keep their left-aligned meta. */
.card--product .card__meta {
  text-align: center;
}
/* Small "Collection" tag on the cover — says what kind of card this is,
   so the photo reads as the hero of a group, not a single product. */
.card__tag {
  position: absolute;
  top: 0.6rem;
  left: 0.6rem;
  z-index: 2;
  padding: 0.26rem 0.5rem;
  border-radius: 100px;
  background: color-mix(in srgb, var(--snow) 62%, transparent);
  border: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
  color: color-mix(in srgb, var(--ink) 60%, transparent);
  font-family: var(--font-mono);
  font-size: 0.52rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  line-height: 1;
  backdrop-filter: blur(6px);
}
/* Collection cards: one master photo + an explicit, always-visible way in.
   The strip sits flush under the image — the card's "door handle". */
.card__explore {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.72rem 0.9rem;
  background: var(--snow);
  border: 1px solid color-mix(in srgb, var(--ink) 14%, transparent);
  border-top: 1px solid color-mix(in srgb, var(--ink) 8%, transparent);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink);
}
.card__explore-arrow {
  transition: transform 0.3s ease;
}
@media (hover: hover) and (pointer: fine) {
  .card--collection:hover .card__explore {
    border-color: color-mix(in srgb, var(--ink) 34%, transparent);
  }
  .card--collection:hover .card__explore-arrow {
    transform: translateX(4px);
  }
}
.card__code {
  font-family: var(--font-mono);
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--c-muted);
}
/* No min-height. It briefly reserved two lines so every price line in a row
   sat level, and that was the wrong trade: only 2 of the 25 tee titles wrap at
   390px, so it padded 23 cards to tidy 2 and left every caption floating in
   the middle of a tall block. Alex called it, correctly, as making things
   worse than the misalignment it fixed (2026-08-13).

   line-height stays stated rather than inherited so the caption's rhythm does
   not drift if the base line-height is ever retuned. */
.card__title {
  font-size: 1.15rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.25;
  margin: 0 0 0.1em;
}
.card__title-text {
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: background-size 0.45s ease;
  padding-bottom: 2px;
}
@media (hover: hover) and (pointer: fine) {
  .card:hover .card__title-text {
    background-size: 100% 1px;
  }
}
.card__sub {
  font-size: 0.9rem;
  color: var(--c-muted);
  margin: 0;
}

/* ============ Brand band (dark) ============ */
.band {
  padding-block: clamp(5rem, 16vh, 12rem);
}
/* Homepage only: pull the shared band into the same section rhythm as
   everything else on the page (other pages keep their own).
   The newsletter used to be listed here too. It is not any more, because
   .newsletter now uses --section-y everywhere: it had been 110px on the
   homepage and 140 on every other page, so the same block sat to a different
   rhythm depending where you met it. */
.home .band {
  padding-block: var(--section-y) 0;
}
/* The mirror of .wearing (see that block for the reasoning behind the ratio
   box). Text left, photograph flush to the RIGHT viewport edge, hairline rule
   between them, 60/40 rather than .wearing's 40/60 so the two panels are the
   same size on opposite sides.

   width:100vw here is now honest: this element no longer carries .container,
   whose padding used to survive the negative margin and hold the picture 48px
   off the edge while this comment claimed otherwise. The gutter lives on
   .band__text instead, which is the only side that needs one. */
.band__inner {
  display: grid;
  grid-template-columns: minmax(0, 60fr) minmax(0, 40fr);
  align-items: stretch;
  width: 100vw;
  margin-inline: calc(50% - 50vw);
}
.band__text {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: clamp(2.5rem, 7vh, 4.5rem);
  padding-inline: var(--gutter) clamp(1.6rem, 4vw, 3.5rem);
  border-right: 1px solid color-mix(in srgb, var(--c-text) 18%, transparent);
}
/* Padding ratio box, not aspect-ratio: as a stretched grid item the ratio
   resolves against a definite 0 cross size in WebKit and the panel collapses.
   Same trap, same fix, same warning as .wearing__media. Do not "simplify". */
.band__media {
  position: relative;
  overflow: hidden;
}
.band__media::before {
  content: "";
  display: block;
  padding-top: 125%;
}
.band__media :is(picture, img) {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.band__copy {
  max-width: 46ch;
  color: var(--c-muted);
}
.band__text .section-title {
  margin-bottom: 1.2rem;
}
/* Cabins teaser inside the idea band — a quiet divided aside, not a
   second full section. */
/* .band__text is a flex column, so a direct-child <a> becomes a block-level
   flex item and stretches: "Read the story" measured 668px against its own
   165px of text, dragging its underline across the whole column. Same reason
   .wearing__cta carries the same line. The second link is inside .band__aside
   and stays inline, which is why only this one was wrong. */
.band__text > .link-arrow {
  align-self: start;
}
.band__aside {
  margin-top: 2.6rem;
  padding-top: 1.7rem;
  /* Capped like .wearing__spec. Unbounded, the rule ran the full 669px column
     above a 360px paragraph, which reads as a stray line rather than a divider
     belonging to the text under it. */
  max-width: 34rem;
  border-top: 1px solid color-mix(in srgb, var(--c-text) 18%, transparent);
}
.band__aside-copy {
  color: var(--c-muted);
  font-size: 0.92rem;
  line-height: 1.55;
  max-width: 42ch;
  margin: 0 0 1rem;
}
@media (max-width: 900px) {
  .band__inner {
    grid-template-columns: 1fr;
  }
  /* Photograph first when stacked, matching .wearing directly above, which is
     image-then-text on a phone. DOM order stays text-first because that is
     what puts the picture on the RIGHT on desktop; order only moves it
     visually, so the reading order for assistive tech is unchanged.

     It also fixes a load problem the mirror introduced: last in the section,
     the photo sat 542px of text further down the page and had not started
     loading when the section came into view, while .wearing's had. */
  .band__media {
    order: -1;
  }
  /* Stacked, the rule would run across the page under the text instead of
     between two columns, and the right-hand inset would read as a stray
     margin. Both go; the photograph carries its own 4:5 height in flow. */
  .band__text {
    border-right: 0;
    padding-inline: var(--gutter);
    padding-block: clamp(2rem, 6vh, 3rem) clamp(2.6rem, 8vh, 4rem);
  }
}

/* ============ Newsletter ============ */
.newsletter {
  padding-block: var(--section-y);
}
/* This block used to override .container with `max-width: 640px;
   margin-inline: 0 auto`, which pinned it to the viewport gutter instead of
   the container. Invisible while the container was 1440 — both landed on
   48px — and 90px out of line with every other section the moment it widened.
   Cap the content, not the container, so the left edge is the page's. */
.newsletter__inner > * {
  max-width: 640px;
}
.newsletter__copy {
  color: var(--c-muted);
  max-width: 44ch;
}
.newsletter__form {
  display: flex;
  gap: 0;
  margin-top: 2rem;
  max-width: 480px;
}
.newsletter__input {
  flex: 1;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  padding: 1rem 1.2rem;
  border: 1px solid var(--c-muted);
  border-right: none;
  background: var(--snow);
  color: var(--ink);
  outline-color: var(--moss);
}
.newsletter__note {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--moss);
  margin-top: 1rem;
}
/* On a dark newsletter (homepage), plain moss is too dim — lift it. */
.newsletter[data-theme="dark"] .newsletter__note {
  color: color-mix(in srgb, var(--moss) 45%, var(--birch));
}
/* Privacy line under the form. Same mono family as the success note so the two
   read as one voice, a step smaller and in muted rather than moss so it sits
   below the form instead of competing with it. Both --c-muted and the rule
   colour are palette variables, so this follows the section into dark on the
   homepage without a second declaration. 52ch keeps it to two lines at the
   640px the newsletter column is capped to. */
.newsletter__fine {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  line-height: 1.7;
  color: var(--c-muted);
  max-width: 52ch;
  margin-top: 1rem;
}
.newsletter__fine a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
}
.newsletter__fine a:hover {
  color: var(--c-text);
  text-decoration-color: currentColor;
}

/* ============ Page head (shop/story) ============ */
.page-head {
  padding-top: clamp(9rem, 24vh, 16rem);
  padding-bottom: clamp(3rem, 10vh, 8rem);
}
.page-head__title {
  font-size: clamp(3rem, 8vw, 8rem);
}

/* ============ PDP ============ */
.pdp {
  padding-top: clamp(7rem, 16vh, 11rem);
  padding-bottom: clamp(4rem, 10vh, 8rem);
}
.pdp__inner {
  display: grid;
  grid-template-columns: 7fr 5fr;
  grid-template-rows: auto 1fr;
  gap: clamp(2rem, 6vw, 6rem);
  column-gap: clamp(2rem, 6vw, 6rem);
  row-gap: 2rem;
  align-items: start;
}
.pdp__head {
  grid-column: 2;
  grid-row: 1;
}
.pdp__media {
  grid-column: 1;
  grid-row: 1 / 3;
  display: grid;
  gap: var(--gutter);
}
/* The worn shot sits under the gallery in the same column, so the left side
   reads as one run of imagery and the buy box on the right stays put. */
.pdp__life {
  grid-column: 1;
  margin: 0;
}
.pdp__life :is(img, picture) {
  display: block;
  width: 100%;
  height: auto;
}
.pdp__img {
  background: var(--snow);
  aspect-ratio: 4 / 5;
  overflow: hidden;
}
.pdp__img img,
.pdp__img picture {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* ---- PDP colour-reactive gallery ---- */
.pdp__gallery {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}
.pdp__stage {
  position: relative;
  background: var(--snow);
  aspect-ratio: 4 / 5;
  overflow: hidden;
  outline: none;
}
.pdp__stage-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  cursor: zoom-in;
}
.pdp__stage-img.is-in {
  opacity: 1;
}
.pdp__zoom-hint {
  position: absolute;
  top: 0.8rem;
  right: 0.8rem;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: color-mix(in srgb, var(--snow) 82%, transparent);
  color: var(--ink);
  border: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
  opacity: 0;
  transform: scale(0.9);
  transition: opacity 0.25s ease, transform 0.25s ease;
  pointer-events: none;
  backdrop-filter: blur(4px);
}
.pdp__stage:hover .pdp__zoom-hint,
.pdp__stage:focus-visible .pdp__zoom-hint {
  opacity: 0.92;
  transform: scale(1);
}
.pdp__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 42px;
  height: 42px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--snow) 82%, transparent);
  border: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
  color: var(--ink);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  backdrop-filter: blur(4px);
  transition: background 0.2s ease, opacity 0.2s ease;
}
.pdp__nav:hover {
  background: var(--snow);
}
.pdp__nav--prev { left: 0.7rem; }
.pdp__nav--next { right: 0.7rem; }
.pdp__nav[hidden] { display: none; }
.pdp__viewtag {
  position: absolute;
  left: 0.8rem;
  bottom: 0.8rem;
  margin: 0;
  padding: 0.32rem 0.6rem;
  background: color-mix(in srgb, var(--ink) 78%, transparent);
  color: var(--snow);
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  border-radius: 2px;
}
.pdp__dots {
  display: flex;
  gap: 0.5rem;
  justify-content: center;
  min-height: 10px;
}
.pdp__dot-nav {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: none;
  padding: 0;
  background: color-mix(in srgb, var(--ink) 22%, transparent);
  cursor: pointer;
  transition: background 0.2s ease, transform 0.2s ease;
}
.pdp__dot-nav.is-active {
  background: var(--ink);
  transform: scale(1.25);
}
/* Thumbnail strip (2026-08-14). Replaces the dot row on products whose gallery
   carries worn shots: a dot can say "there is another view", a thumbnail says
   which one, and that is the whole reason the worn photographs are in the
   gallery instead of at the foot of the page. Left-aligned, not centred like
   the dots — it reads as a filmstrip under the image. */
.pdp__thumbs {
  justify-content: flex-start;
  gap: 0.5rem;
  min-height: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.pdp__thumbs::-webkit-scrollbar { display: none; }
.pdp__thumb {
  flex: 0 0 auto;
  width: 68px;
  aspect-ratio: 1;
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
  background: var(--snow);
  cursor: pointer;
  overflow: hidden;
  transition: border-color 0.2s ease;
}
.pdp__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.pdp__thumb.is-active {
  border-color: var(--moss);
  box-shadow: inset 0 0 0 1px var(--moss);
}

.pdp__details {
  grid-column: 2;
  grid-row: 2;
  position: sticky;
  top: 6rem;
}
.pdp__title {
  font-size: clamp(2.2rem, 4vw, 3.6rem);
}
.pdp__sub {
  color: var(--c-muted);
}
.pdp__code {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.pdp__price {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.6rem;
  margin: 1rem 0;
}
.pdp__desc {
  max-width: 46ch;
}
.pdp__colors {
  display: flex;
  gap: 0.45rem;
  flex-wrap: wrap;
  margin: 1.4rem 0 0.4rem;
}
.pdp__dot {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--dot);
  border: 1px solid color-mix(in srgb, var(--ink) 18%, transparent);
}
.pdp__sizes {
  display: flex;
  gap: 0.5rem;
  margin: 1.2rem 0 0;
  flex-wrap: wrap;
}
.pdp__size {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  padding: 0.55rem 0.9rem;
  border: 1px solid color-mix(in srgb, var(--c-muted) 55%, transparent);
  color: var(--c-muted);
}
.pdp__add {
  width: 100%;
  max-width: 360px;
}

/* Creator 2.0 size guide — a garment-spec sheet in the same restrained,
   survey-drawing language as the cabin maps. Native details/summary keeps it
   keyboard-friendly and useful without JavaScript; JS only switches units. */
.size-guide {
  max-width: 460px;
  margin: 1rem 0 0.8rem;
  border-block: 1px solid color-mix(in srgb, var(--ink) 16%, transparent);
}
.size-guide__summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.85rem 1.7rem 0.85rem 0;
  cursor: pointer;
  list-style: none;
  position: relative;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--moss);
}
.size-guide__summary::-webkit-details-marker { display: none; }
.size-guide__summary::after {
  content: "+";
  position: absolute;
  right: 0;
  top: 50%;
  translate: 0 -50%;
  font-size: 1.1rem;
  font-weight: 400;
}
.size-guide[open] .size-guide__summary::after { content: "−"; }
.size-guide__summary:focus-visible {
  outline: 2px solid var(--moss);
  outline-offset: 4px;
}
.size-guide__summary-fit {
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--c-muted);
}
.size-guide__body { padding: 0.55rem 0 1.1rem; }
.size-guide__intro {
  padding: 0.35rem 0 1rem;
}
.size-guide__intro p {
  margin: 0;
  color: var(--c-muted);
  font-size: 0.82rem;
  line-height: 1.45;
}
.size-guide__intro .size-guide__lead {
  margin-bottom: 0.35rem;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 700;
  line-height: 1.15;
}
/* Measuring guide: one shirt and one unmistakable dimension per panel.
   Capped rules replace arrowheads; plain-language captions replace A/B/C. */
.measurement-guide {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(0.55rem, 2vw, 1.3rem);
  margin: 0 0 1.3rem;
}
.measurement-guide__figure {
  min-width: 0;
  margin: 0;
}
.measurement-guide__drawing {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}
.measurement-guide__shirt {
  fill: color-mix(in srgb, var(--c-text) 4%, transparent);
  stroke: var(--c-muted);
  stroke-width: 1.4;
  vector-effect: non-scaling-stroke;
}
.measurement-guide__seam {
  fill: none;
  stroke: var(--c-muted);
  stroke-width: 1.1;
  opacity: 0.52;
  vector-effect: non-scaling-stroke;
}
.measurement-guide__measure,
.measurement-guide__cap {
  fill: none;
  stroke: var(--gold);
  stroke-width: 2.5;
  stroke-linecap: square;
  vector-effect: non-scaling-stroke;
}
.measurement-guide__point { fill: var(--gold); }
.measurement-guide figcaption {
  margin-top: 0.45rem;
  text-align: center;
  font-size: 0.7rem;
  line-height: 1.35;
}
.measurement-guide figcaption strong,
.measurement-guide figcaption span { display: block; }
.measurement-guide figcaption strong {
  color: var(--c-text);
  font-weight: 700;
}
.measurement-guide figcaption span {
  margin-top: 0.15rem;
  color: var(--c-muted);
}
.size-guide__table-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.55rem;
}
.size-guide__model {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  color: var(--c-muted);
}
.size-guide__units { display: none; }
html.js .size-guide__units {
  display: inline-flex;
  border: 1px solid color-mix(in srgb, var(--ink) 18%, transparent);
}
.size-guide__units button {
  min-width: 2.35rem;
  min-height: 30px;
  padding: 0.25rem 0.45rem;
  border: 0;
  background: transparent;
  color: var(--c-muted);
  font-family: var(--font-mono);
  font-size: 0.66rem;
  cursor: pointer;
}
.size-guide__units button[aria-pressed="true"] {
  background: var(--moss);
  color: var(--snow);
}
.size-guide__units button:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}
.size-guide__table-wrap {
  overflow-x: auto;
  scrollbar-width: thin;
}
.size-guide__table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-variant-numeric: tabular-nums;
}
.size-guide__table th,
.size-guide__table td {
  padding: 0.55rem 0.35rem;
  text-align: right;
  border-bottom: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
  white-space: nowrap;
}
.size-guide__table thead th {
  color: var(--c-muted);
  font-size: 0.58rem;
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.size-guide__table th:first-child,
.size-guide__table td:first-child { text-align: left; }
.size-guide__table tbody th {
  color: var(--moss);
  font-weight: 700;
}
[data-unit-value="in"] { display: none; }
.size-guide[data-unit="in"] [data-unit-value="cm"] { display: none; }
.size-guide[data-unit="in"] [data-unit-value="in"] { display: inline; }
.size-guide__note {
  margin: 0.8rem 0 0;
  color: var(--c-muted);
  font-size: 0.72rem;
  line-height: 1.45;
}
.size-guide__help {
  max-width: 460px;
  margin: 0 0 1.8rem;
  padding: 0.15rem 0 0.15rem 0.85rem;
  border-left: 2px solid var(--gold);
  color: var(--c-muted);
  font-size: 0.78rem;
  line-height: 1.5;
}
.size-guide__help p { margin: 0; }
.size-guide__help p + p { margin-top: 0.45rem; }
.size-guide__help a {
  color: var(--moss);
  font-weight: 700;
  text-underline-offset: 0.16em;
}
.size-guide__help a:focus-visible {
  outline: 2px solid var(--moss);
  outline-offset: 3px;
}
@media (max-width: 430px) {
  .size-guide__summary-fit { display: none; }
  .size-guide .measurement-guide {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.75rem 0.45rem;
    margin-bottom: 1rem;
  }
  .size-guide .measurement-guide__figure:last-child {
    grid-column: 1 / -1;
    width: calc(50% - 0.225rem);
    justify-self: center;
  }
  .size-guide .measurement-guide figcaption { font-size: 0.68rem; }
  .size-guide__table { font-size: 0.64rem; }
  .size-guide__table th,
  .size-guide__table td { padding-inline: 0.22rem; }
}
.pdp__note {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--moss);
  margin-top: 0.8rem;
}
.pdp__specs {
  margin-top: 2.5rem;
  border-top: 1px solid color-mix(in srgb, var(--c-muted) 40%, transparent);
}
.pdp__spec {
  display: flex;
  justify-content: space-between;
  gap: 2rem;
  padding: 0.8rem 0;
  border-bottom: 1px solid color-mix(in srgb, var(--c-muted) 40%, transparent);
}
.pdp__spec dt {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--c-muted);
}
.pdp__spec dd {
  margin: 0;
  font-size: 0.9rem;
  text-align: right;
}
@media (max-width: 900px) {
  .pdp__inner {
    grid-template-columns: 1fr;
    grid-template-rows: none;
    row-gap: 1.6rem;
  }
  .pdp__head,
  .pdp__media,
  .pdp__details {
    grid-column: auto;
    grid-row: auto;
  }
  .pdp__details {
    position: static;
  }
  /* Swipeable gallery: one image tall, peek of the next slide */
  .pdp__media {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    gap: 0.8rem;
    margin-inline: calc(-1 * var(--gutter));
    padding-inline: var(--gutter);
    scrollbar-width: none;
  }
  .pdp__media::-webkit-scrollbar {
    display: none;
  }
  .pdp__media .pdp__img {
    flex: 0 0 86%;
    scroll-snap-align: center;
  }

  /* The USP bar is gone from this page and the mockup is what a phone opens
     for, so the head no longer needs a hero's worth of air above it
     (Alex, 2026-08-14). 135px → 72px on a 844px-tall phone. */
  .pdp {
    padding-top: clamp(3.5rem, 7vh, 5rem);
  }
  /* Tighter than the 2rem the desktop grid uses between every row: on a phone
     these are three parts of one thing (name, mockup, picker), and 2rem three
     times over was what pushed the swatches under the fold. */
  .pdp__inner {
    row-gap: 1.2rem;
  }

  /* ---- The phone product page (Alex, 2026-08-13, reworked and rolled out to
     every product 2026-08-14) ----
     The order used to run gallery → worn shots → description → buy box, which
     put the colour swatches 671px below the image they change. You had to
     scroll down to pick a colour and back up to see it, twice per colour.

     Trialled on Moose Tips behind a `pdpTrial` editorial flag; the flag is gone
     now that every product uses this, so a new product inherits it by existing.

     Pure source ordering, no markup moved and no JS: .pdp__inner is already a
     grid and its children are grid items, so `order` reaches them, and
     .pdp__details becomes a flex column for the same reason. That matters —
     cart.js binds the swatches by querying inside [data-buybox] AFTER an await,
     so physically moving the node out of the buy box would silently unbind
     add-to-cart. Nothing here moves a node. */
  .pdp .pdp__head { order: 1; }
  .pdp .pdp__media { order: 2; }
  .pdp .pdp__details { order: 3; }
  .pdp .pdp__details {
    display: flex;
    flex-direction: column;
  }
  /* The price rides on the title's line (Alex, 2026-08-14), which is the row the
     description then gets to use. The eyebrow and the colourway line are hidden
     just below, so this flexbox only ever holds those two. */
  .pdp .pdp__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.8rem;
    /* No wrap. Long names ("Weekend in the Woods Hoodie") would otherwise push
       the price onto a row of its own, which costs the row this arrangement
       exists to save. The title shrinks and wraps inside its own box instead,
       and the price stays anchored to the first line. min-width: 0 is what lets
       a flex item shrink below its longest word. */
    flex-wrap: nowrap;
  }
  .pdp .pdp__head .pdp__title {
    min-width: 0;
  }
  .pdp .pdp__head .pdp__price {
    margin: 0;
    font-size: 1.35rem;
    white-space: nowrap;
    flex: 0 0 auto;
  }
  /* Swatches under the thumbnails, against the image they change, and the
     description below them in full (Alex, 2026-08-14). Judged on Quiet Is a
     Destination first, then rolled out to every product.

     The description is NOT clamped: two lines with a "Read more" read as a
     teaser for copy that is three sentences long, which is not worth a
     control. It runs whole, under the buy block. */
  .pdp .buybox { order: 1; }
  .pdp .pdp__desc {
    order: 2;
    margin-top: 1.8rem;
  }
  .pdp .pdp__specs { order: 3; }
  .pdp .buybox__picked { margin-top: 0.8rem; }
  .pdp .pdp__colors { margin-top: 0.7rem; }
  /* The page opens on the product's own name (Alex, 2026-08-14). The collection
     eyebrow is one tap back up the breadcrumb and repeats on the shelf you came
     from; the colourway count is answered by the swatches you are about to
     scroll to, and the product code is an internal reference. Between them they
     cost 120px above the mockup, which is the one thing a phone opens for.
     Both still render on desktop, where the column has the room. */
  .pdp .pdp__head .eyebrow,
  .pdp .pdp__sub {
    display: none;
  }
  /* The worn shots are in the gallery strip now, so the big copies below would
     be the same two photographs twice on one phone screen's worth of scroll.
     Desktop keeps them: there they run down the left column beside the buy box
     and are the reason that layout reads as one piece of imagery. */
  .pdp .pdp__life { display: none; }
}
.related {
  padding-block: clamp(4rem, 10vh, 8rem);
}
.shop__section {
  padding-block: clamp(2rem, 6vh, 4rem);
}
.shop__blurb {
  color: var(--c-muted);
  max-width: 46ch;
  margin-bottom: 2.5rem;
}
.card--collection .card__sub {
  max-width: 34ch;
}
.related .grid {
  margin-top: 2rem;
}

/* ============ Story page ============ */
.story-hero {
  padding-top: clamp(10rem, 30vh, 18rem);
  padding-bottom: clamp(4rem, 12vh, 9rem);
}
.story-hero__title {
  font-size: clamp(3rem, 9vw, 9.5rem);
}
.story-hero__lede {
  color: var(--c-muted);
  max-width: 42ch;
  font-size: 1.1rem;
}

/* ============ Cabins hero: aurora field ============ */
.story-hero--cabins {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  min-height: clamp(560px, 90vh, 980px);
  display: flex;
  align-items: center;
  padding-top: clamp(6rem, 14vh, 9rem);
  padding-bottom: clamp(4rem, 10vh, 7rem);
}
/* Two things the removed .story-hero__grid wrapper used to provide, both of
   which broke the moment it went:
   1. width:100% — this section is a flex container, so a shrink-to-fit child
      gets centred by its auto margins and the copy lands 160px right of where
      every other hero starts (the same trap .games-hero--photo documents).
   2. position/z-index — .story-hero__media is absolutely positioned, so it
      paints over static content. Without this the eyebrow and lede vanished
      behind the video while the title stayed visible, because GSAP puts a
      transform on its line spans and that lifts them into the painted layer.
      The DOM still reported opacity:1, so only a screenshot caught it. */
.story-hero--cabins > .container {
  position: relative;
  z-index: 1;
  width: 100%;
}
.story-hero--cabins .story-hero__title { font-size: clamp(2.6rem, 5.5vw, 5.6rem); }
/* The lede is one sentence that overshot 42ch by a single word, so the wrap
   dropped "villas." onto a line of its own. Balance splits it evenly instead. */
.story-hero--cabins .story-hero__lede { text-wrap: balance; }
@media (max-width: 880px) {
  .story-hero--cabins { min-height: 0; }
}

/* Northern-lights hero video behind the cabins hero (replaces the canvas
   aurora there — the footage is the real thing). Shade keeps copy legible and
   fades to black at the bottom seam into the dark map section. */
.story-hero__media {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background: var(--norr-black);
}
.story-hero__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Phones get a still instead of the video: autoplay is not guaranteed there
   and Low Power Mode refuses it outright, which showed a black hero. Swapped
   by width rather than by pointer — this is about the device playing video,
   and a wide touchscreen still autoplays fine. */
/* Scoped to .story-hero__media on purpose: the generic
   `.story-hero__media picture` rule below is (0,1,1) and would otherwise
   outrank a bare .story-hero__still, leaving the 1x1 fallback stretched over
   the video on desktop instead of hidden. */
.story-hero__media .story-hero__still { display: none; }
@media (max-width: 700px) {
  .story-hero--cabins .story-hero__video,
  .story-hero--mobile-still .story-hero__video { display: none; }
  .story-hero__media .story-hero__still { display: block; position: absolute; inset: 0; }
  .story-hero__media .story-hero__still img { width: 100%; height: 100%; object-fit: cover; display: block; }
  /* Tall enough to hold the whole photograph. min-height is 0 below 880px, so
     the hero sizes to its copy: once the coordinates went and the lede was
     halved, it collapsed to 382px and `cover` ate 27% off the bottom of a
     portrait picture. 4:3 is the still's own aspect, so at 100vw wide this is
     exactly its height and nothing is cropped. Capped because the same sum on
     a 700px tablet would ask for 933px of hero. */
  .story-hero--cabins { min-height: min(133.33vw, 640px); }
  /* Copy to the foot of the frame, not its middle. Centred, it landed on the
     sunset and the lake — the brightest band in the picture — and no veil
     light enough to keep the colour could carry it there. At the bottom it
     sits over shadowed grass and stone, so the gradient's own dark end does
     the work and the sky is left alone. */
  .story-hero--cabins { align-items: flex-end; }
  /* Light. The photograph is the point here and its colour is what carries it,
     so the veil only takes the edge off and the text-shadow below does the
     legibility work. Measured after: title 4.3:1, lede 3.5:1 against the
     brightest 5% of their own bands. */
  .story-hero--cabins .story-hero__shade {
    background: linear-gradient(to bottom,
      rgba(12, 17, 16, 0.42) 0%,
      rgba(12, 17, 16, 0.26) 38%,
      rgba(12, 17, 16, 0.58) 78%,
      var(--norr-black) 100%);
  }
  /* A photograph puts anything behind the text, so a wide soft shadow does the
     rest rather than a heavier veil dulling the picture — the same trick
     .explore-card__title uses over its photos. */
  .story-hero--cabins .story-hero__title,
  .story-hero--cabins .story-hero__lede {
    text-shadow:
      0 1px 28px rgba(12, 17, 16, 0.9),
      0 1px 6px rgba(12, 17, 16, 0.7),
      0 0 2px rgba(12, 17, 16, 0.5);
  }
}
/* image variant of the media layer (story hero photo) */
.story-hero__media picture { position: absolute; inset: 0; display: block; }
.story-hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 42%;
  display: block;
}
.story-hero--photo {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  min-height: clamp(520px, 84vh, 880px);
  display: flex;
  align-items: flex-end;
  padding-top: clamp(6rem, 12vh, 9rem);
  padding-bottom: clamp(3rem, 9vh, 7rem);
}
.story-hero--photo > .container { position: relative; z-index: 1; }
/* Centred, because the desktop file is already cropped to the composition
   (see the note in story.njk). The generic `.story-hero__media img` rule
   below sets `center 42%` for the cabins photograph and would otherwise
   apply here too.

   An earlier attempt anchored the portrait original to the bottom instead.
   It framed correctly at 1440x756 and cut the man's head off at 1920x778,
   because how much of a portrait file survives `cover` depends on the
   window: 40% of its height in the first case, 31% in the second. No single
   object-position value survives that, which is why the crop is now cut at
   build time rather than chosen by the browser. */
.story-hero--photo .story-hero__media img { object-position: center; }
@media (max-width: 700px) {
  /* Copy to the top on phones, Alex's own call (2026-08-15): the portrait
     crop puts the whole sky in frame, and the block sits in it rather than
     over the snow. Every other hero on the site keeps its copy at the foot;
     this one is deliberately the exception.

     The veil has to move with it. The shared mobile shade thins to 0.2 by
     40% and the copy now sits from 20% to 62%, which measured 2.99:1 on the
     title and 1.28:1 on the lede against the brightest 5% of their own
     bands. The lede was effectively invisible. This holds the veil across
     the band the copy occupies and lets it go again over the treeline, so
     the sunset still carries. Same text-shadow trick as the cabins hero:
     shadow does the work a heavier veil would otherwise have to. */
  .story-hero--photo { align-items: flex-start; }
  .story-hero--photo .story-hero__shade {
    background:
      linear-gradient(to bottom,
        rgba(12, 17, 16, 0.74) 0%,
        rgba(12, 17, 16, 0.70) 46%,
        rgba(12, 17, 16, 0.34) 70%,
        rgba(12, 17, 16, 0.30) 82%,
        var(--norr-black) 100%);
  }
  /* Fog on the lede measured 2.89:1 even with the veil above, because fog is
     a mid grey and the sky behind it is the brightest thing on the page.
     Birch instead: 5.4:1, and no further darkening of a picture whose sky is
     the reason it is here. */
  .story-hero--photo .story-hero__lede { color: var(--birch); }
  /* Not the title. Its lines sit inside `.line-mask`, which clips for the
     GSAP reveal, so a shadow on them is cut off square and draws a visible
     rectangle around "is honest." The veil alone measures 8.91:1 there, so
     the title does not need one. */
  .story-hero--photo .story-hero__lede,
  .story-hero--photo .story-hero__place {
    text-shadow:
      0 1px 28px rgba(12, 17, 16, 0.9),
      0 1px 6px rgba(12, 17, 16, 0.7),
      0 0 2px rgba(12, 17, 16, 0.5);
  }
}
.story-hero--photo .story-hero__title { font-size: clamp(2.8rem, 6.5vw, 6.5rem); }
.story-hero__shade {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(100deg, rgba(12, 17, 16, 0.86) 0%, rgba(12, 17, 16, 0.46) 40%, rgba(12, 17, 16, 0.08) 74%),
    linear-gradient(to bottom, rgba(12, 17, 16, 0) 52%, var(--norr-black) 100%);
}
@media (max-width: 880px) {
  .story-hero__shade {
    background:
      linear-gradient(to bottom, rgba(12, 17, 16, 0.55) 0%, rgba(12, 17, 16, 0.2) 40%, var(--norr-black) 100%);
  }
}

/* Canvas aurora behind the hero + a dark veil so the copy stays legible. */
.aurora {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
.aurora__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0.5;
  filter: blur(8px) saturate(0.85);
  mix-blend-mode: screen;
}
.aurora__veil {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(100deg, rgba(12, 17, 16, 0.82) 0%, rgba(12, 17, 16, 0.42) 38%, rgba(12, 17, 16, 0.06) 72%),
    linear-gradient(to bottom, rgba(12, 17, 16, 0) 55%, var(--norr-black) 100%);
}
@media (max-width: 760px) {
  .aurora__canvas { opacity: 0.44; filter: blur(9px) saturate(0.7); }
  .aurora__veil {
    background: linear-gradient(to bottom, rgba(12, 17, 16, 0.5) 0%, rgba(12, 17, 16, 0.15) 42%, var(--norr-black) 100%);
  }
}

/* Aurora rolled out to the other dark heroes (Story hero + per-cabin page). */
.story-hero--aurora,
.cabin--aurora {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}
.story-hero--aurora > .container,
.cabin--aurora > .container {
  position: relative;
  z-index: 1;
}
.aurora--band {
  inset: 0 0 auto 0;
  height: min(100%, 860px);
}
.aurora--soft .aurora__veil {
  background: linear-gradient(to bottom, rgba(12, 17, 16, 0.28) 0%, rgba(12, 17, 16, 0.5) 42%, var(--norr-black) 100%);
}
.chapter {
  padding-block: clamp(4rem, 12vh, 9rem) 0;
}
.chapter__inner {
  display: grid;
  grid-template-columns: 7fr 5fr;
  gap: clamp(2rem, 6vw, 6rem);
  align-items: center;
}
.chapter--flip .chapter__inner {
  grid-template-columns: 5fr 7fr;
}
.chapter--flip .chapter__media {
  order: 2;
}
.chapter__text p {
  color: var(--c-muted);
  max-width: 46ch;
}
@media (max-width: 900px) {
  .chapter__inner,
  .chapter--flip .chapter__inner {
    grid-template-columns: 1fr;
  }
  .chapter--flip .chapter__media {
    order: 0;
  }
}
.manifesto {
  padding-block: clamp(6rem, 20vh, 14rem) 0;
}
.manifesto__title {
  font-size: clamp(2.6rem, 7vw, 7rem);
  color: var(--c-text);
}
.story-outro {
  padding-block: clamp(6rem, 18vh, 12rem);
}
.story-outro .btn {
  margin-top: 1rem;
}

/* ---- Story: the true origin (2026-08-14) ----
   The page runs dark from the hero through the season slider, then takes its
   one palette turn at the decision chapter (data-dawn), so the emotional turn
   and the visual turn land on the same scroll. Everything below reuses the
   site's motion (.reveal, .lines) and tokens; no new colours, no new type. */
.story-hero__place {
  margin-bottom: 1.2rem;
}
/* Chapter opener: mark, one big headline, copy hung off the right edge.
   Chapters are the ONLY numbered thing on the page. */
.stchapter {
  padding-top: calc(var(--section-y) * 1.3);
}
.stchapter__title {
  font-size: clamp(2.8rem, 7vw, 7rem);
  margin: 0;
}
.stchapter__body {
  max-width: 44ch;
  margin: clamp(2rem, 5vh, 3.5rem) 0 0 auto;
}
.stchapter__body p {
  color: var(--c-muted);
}
.stchapter__body p:last-child {
  margin-bottom: 0;
}
/* Full-bleed scenes: the media half runs to the viewport edge and the full
   section height, the other half carries one statement and a lot of air.
   Consecutive scenes butt together into a checkerboard filmstrip — those
   boundaries are deliberate hard cuts, so no --section-y between them. */
.scene {
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  min-height: 92vh;
  margin-top: var(--section-y);
}
.scene + .scene {
  margin-top: 0;
}
.scene__media {
  position: relative;
  overflow: hidden;
}
.scene__media picture {
  position: absolute;
  inset: 0;
}
.scene__media img,
.scene__media video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.scene--flip .scene__media {
  order: 2;
}
.scene__panel {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(3rem, 7vw, 6rem);
}
.scene__line {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.9rem, 3.4vw, 3.2rem);
  letter-spacing: -0.02em;
  line-height: 1.12;
  max-width: 15ch;
  margin: 0;
}
.scene__sub {
  color: var(--c-muted);
  max-width: 36ch;
  margin: 1.2rem 0 0;
}
.scene__cap {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--c-muted);
  border-top: 1px solid color-mix(in srgb, var(--c-muted) 35%, transparent);
  max-width: 30ch;
  margin: clamp(2.5rem, 6vh, 4rem) 0 0;
  padding-top: 0.7rem;
}
.stmedia {
  margin: 0;
}
.stmedia video,
.stmedia img {
  width: 100%;
  display: block;
  border-radius: 10px;
}
.stmedia picture {
  display: block;
}
.stmedia__cap {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--c-muted);
  border-top: 1px solid color-mix(in srgb, var(--c-muted) 35%, transparent);
  margin-top: 0.9rem;
  padding-top: 0.7rem;
}
/* Statement beat: a full-width line that closes a chapter. */
.stbeat {
  padding: calc(var(--section-y) * 1.2) var(--gutter) 0;
  text-align: center;
}
.stbeat p {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.9rem, 4vw, 3.6rem);
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin: 0;
}
.stbeat p + p {
  color: var(--c-muted);
}
/* Season slider: both frames are portrait phone shots of the same dock, so
   the widget goes portrait too instead of centre-cropping them to 3:2.
   Doubled class: the base .ba rules live LATER in this file, so a plain
   .ba--dock loses the tie and silently does nothing. */
.ba.ba--dock {
  max-width: 620px;
  aspect-ratio: 3 / 4;
}
/* The decision: the dawn chapter. Small mono setting line, the biggest
   headline on the page, then the answer in moss — the one accent moment. */
.stdec {
  padding-top: calc(var(--section-y) * 1.4);
}
.stdec__setting {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  line-height: 1.9;
  color: var(--c-muted);
  max-width: 26rem;
  margin: 0 0 clamp(2rem, 5vh, 3.5rem) auto;
}
.stdec__title {
  font-size: clamp(2.6rem, 6.5vw, 6.5rem);
  max-width: 14ch;
  margin-bottom: 0;
}
.stdec__grid {
  display: grid;
  grid-template-columns: minmax(280px, 0.9fr) minmax(280px, 1.1fr);
  gap: clamp(2.5rem, 7vw, 8rem);
  align-items: center;
  max-width: 1180px;
  margin: clamp(3rem, 8vh, 6rem) auto 0;
}
.stdec__copy p {
  color: var(--c-muted);
  max-width: 44ch;
}
.stdec__answer {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(2rem, 4.2vw, 3.8rem);
  letter-spacing: -0.03em;
  line-height: 1.02;
  color: var(--moss);
  margin: clamp(2.5rem, 6vh, 4rem) 0 0;
}
/* Why clothes: centred, then the first design as a dark panel — the tee
   itself, set in the page. Fixed colours on purpose; the dawn has already
   happened and this rectangle stays night. */
.stwhy {
  padding-top: var(--section-y);
  text-align: center;
}
.stwhy .section-title {
  margin-inline: auto;
  max-width: 16ch;
}
.stwhy__copy {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(2rem, 6vw, 7rem);
  max-width: 62rem;
  margin: clamp(2rem, 5vh, 3.5rem) auto 0;
  text-align: left;
}
.stwhy__copy p {
  color: var(--c-muted);
}
.stfirst {
  width: min(900px, 100%);
  margin: calc(var(--section-y) * 0.8) auto 0;
  padding: clamp(3.5rem, 9vh, 6.5rem) clamp(1.5rem, 5vw, 4rem);
  background: var(--norr-black);
  color: var(--birch);
  border-radius: 16px;
}
.stfirst .eyebrow {
  color: var(--fog);
}
.stfirst__mark {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(2.8rem, 8vw, 6.5rem);
  letter-spacing: -0.04em;
  line-height: 0.95;
  margin: 0 0 1.5rem;
}
.stfirst__sub {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fog);
  margin: 0 0 2rem;
}
.stfirst__link {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  border-bottom: 1px solid color-mix(in srgb, var(--birch) 45%, transparent);
  padding-bottom: 0.3rem;
}
@media (max-width: 900px) {
  .scene {
    grid-template-columns: 1fr;
    min-height: 0;
  }
  .scene__media,
  .scene--flip .scene__media {
    order: 0;
    min-height: 62vh;
  }
  .scene__panel {
    padding: 2.2rem var(--gutter) 3rem;
  }
  .scene__line {
    max-width: none;
  }
  .stdec__grid {
    grid-template-columns: 1fr;
  }
  .stdec__grid .stmedia {
    width: min(480px, 100%);
  }
  .stwhy__copy {
    grid-template-columns: 1fr;
    gap: 0;
  }
}
@media (max-width: 620px) {
  .stdec__setting {
    margin-left: 0;
  }
}

/* ============ Cabins ============ */
.cabfilter {
  padding-bottom: 1rem;
}
.cabfilter__row {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.chip {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: 0.55rem 1rem;
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--c-muted) 55%, transparent);
  color: var(--c-muted);
  cursor: pointer;
  transition: border-color 0.25s ease, color 0.25s ease;
}
/* Hover and ticked used to set the identical two declarations, so a chip under
   the cursor looked ticked whether it was or not: clicking to untick changed
   the grid behind it and nothing about the chip, which reads as "this filter
   cannot be cleared" (Alex, 2026-08-14). Hover now only lifts the border and
   the ticked state fills, so the two can never be confused.
   Gated to hover-capable pointers as well, because on a phone :hover sticks
   after a tap until you tap something else — same false "still on" signal. */
@media (hover: hover) {
  /* Deliberately NOT var(--c-text): that is the ticked border, so a hovered
     unticked chip wore the ticked chip's edge and untick still looked like it
     had not worked (Alex, 2026-08-14, second report). Lifting the rest colour
     to full opacity is a visible hover without borrowing the ticked signal. */
  .chip:hover { border-color: var(--c-muted); }
}
/* Must stay after :hover — equal specificity (0,2,0), so source order decides. */
.chip.is-active {
  background: var(--c-text);
  border-color: var(--c-text);
  color: var(--c-bg);
}
/* A ticked filter says how to clear itself. "All cabins" is excluded: it *is*
   the cleared state, so there is nothing to remove from it. */
.chip.is-active:not([data-filter="all"])::after {
  /* the "/ ''" gives the glyph an empty alt: generated content counts toward
     the accessible name, and aria-pressed already announces the state, so the
     × would otherwise be read out as "multiplication sign" after every label */
  content: "×" / "";
  margin-left: 0.55em;
}
/* The Select filter is the mark, not a feature, so it wears the mark's amber
   rather than the birch every other chip uses. #E2B66F is the same value as
   .cabin-select__badge and .cabfilter__note-mark, so the three read as one.
   These must stay AFTER .chip:hover and .chip.is-active: all three selectors
   weigh the same (0,2,0) and source order is what decides the winner. */
.chip--select {
  color: #e2b66f;
  border-color: color-mix(in srgb, #e2b66f 45%, transparent);
}
@media (hover: hover) {
  .chip--select:hover { border-color: #e2b66f; }
}
.chip--select.is-active {
  background: #e2b66f;
  border-color: #e2b66f;
  color: var(--norr-black);
}
/* A finger needs 44px; the type does not. Padding grows rather than the font,
   so the chips keep their small-caps proportion and only the tap area changes.
   Keyed on the pointer, not the width — a wide touchscreen has the same thumb. */
@media (hover: none) {
  .chip {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-inline: 1.1rem;
  }
}
/* Legend for the gold chip above it. The mark is set in the same amber and
   the same mono small-caps as the chip itself, so the eye joins the two
   without a line or an arrow; the sentence after it is plain body text. */
.cabfilter__note {
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--c-muted);
  margin: 1.1rem 0 0;
  max-width: 52ch;
}
.cabfilter__note-mark {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: #e2b66f;
  margin-right: 0.5rem;
}
.cabfilter__empty {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  color: var(--c-muted);
  margin-top: 1.5rem;
}
.cabin {
  padding-block: clamp(4rem, 10vh, 8rem);
}
.cabin__inner {
  display: grid;
  grid-template-columns: 7fr 5fr;
  gap: clamp(2rem, 5vw, 5rem);
  align-items: start;
}
/* Grid items default to min-width:auto, so the gallery's fixed-width filmstrip
   would push the media column as wide as its content — 3000px+ of page overflow
   instead of a scrolling track. */
.cabin__media { min-width: 0; }
.cabin--flip .cabin__inner {
  grid-template-columns: 5fr 7fr;
}
.cabin--flip .cabin__media {
  order: 2;
}
/* Cabin photo carousel. A native scroll-snap track, not a JS slider: swipe,
   trackpad and keyboard scrolling come free, momentum feels right on touch, and
   the browser only fetches slides as they approach the viewport — which is what
   keeps a twelve-photo gallery off the page-weight budget. cabin-gallery.js only
   adds the arrows and the counter. */
.cabin-gallery {
  position: relative;
  margin-top: var(--gutter);
}
.cabin-gallery__track {
  display: flex;
  gap: var(--gutter);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  outline: none;
  /* the track scrolls sideways; the page keeps the vertical axis */
  overscroll-behavior-x: contain;
}
.cabin-gallery__track::-webkit-scrollbar { display: none; }
/* A filmstrip: fixed row height, every photo at its natural aspect — portrait
   slides narrow, landscape wide, nothing cropped by anyone. Two fixed-frame
   designs were tried and rejected: the archive is overwhelmingly portrait (some
   cabins have a single landscape shot), so any uniform landscape frame discards
   half of most photos and the gallery stops matching the listing it came from.
   The row height sits near the old two-thumb strip's, so the page keeps its
   proportions. */
.cabin-gallery__slide {
  flex: 0 0 auto;
  scroll-snap-align: start;
  height: clamp(150px, 17vw, 240px);
  /* width falls out of the inline aspect-ratio × this height — never out of the
     image, which has no intrinsic size until its lazy load fires */
  overflow: hidden;
  margin: 0;
}
.cabin-gallery__slide :where(img) {
  width: 100%;
  height: 100%;
  object-fit: cover; /* box already matches the photo's ratio; absorbs 1px rounding */
  display: block;
}
.cabin-gallery__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  border: 1px solid rgba(238, 237, 232, 0.25);
  border-radius: 50%;
  background: color-mix(in srgb, var(--norr-black) 62%, transparent);
  color: var(--birch);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.25s ease, border-color 0.25s ease, background 0.25s ease;
}
.cabin-gallery__nav--prev { left: 14px; }
.cabin-gallery__nav--next { right: 14px; }
.cabin-gallery:hover .cabin-gallery__nav,
.cabin-gallery__nav:focus-visible { opacity: 1; }
.cabin-gallery__nav:hover { border-color: var(--birch); background: var(--norr-black); }
.cabin-gallery__nav[disabled] { opacity: 0 !important; pointer-events: none; }
/* Touch has no hover, so the arrows would never appear — swipe is the gesture
   there anyway, and the counter still says how far along you are. */
@media (hover: none) {
  .cabin-gallery__nav { display: none; }
}
.cabin-gallery__count {
  margin-top: 0.75rem;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--c-muted);
}
@media (prefers-reduced-motion: reduce) {
  .cabin-gallery__track { scroll-behavior: auto; }
  .cabin-gallery__nav { transition: none; }
}
.cabin__region {
  color: var(--c-muted);
  font-size: 0.95rem;
  margin-top: -0.4rem;
}
/* Norrwild Select — places we've stayed in ourselves. Warm amber underlay
   (amber-as-light, per DESIGN.md) so the mark reads as earned, not decorative. */
.cabin-select {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin: 0.4rem 0 1.4rem;
}
.cabin-select__badge {
  flex: none;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: #e2b66f;
  background: color-mix(in srgb, #e2b66f 13%, transparent);
  border: 1px solid color-mix(in srgb, #e2b66f 40%, transparent);
  padding: 0.4rem 0.66rem;
  white-space: nowrap;
}
.cabin-select__note {
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--c-muted);
  max-width: 44ch;
  margin: 0 0 1.4rem;
}
/* The badge row carries the full gap to the lede, unless a note follows it, in
   which case the note carries it instead. `:has` and not `:last-of-type` — the
   lede is a <p> too, so last-of-type never matches the badge row. */
.cabin-select:has(+ .cabin-select__note) { margin-bottom: 0.7rem; }
/* Same shape as Select, fog instead of amber — see .cabincard__select--showcase.
   A badge-level modifier now, not a parent one: a place can carry both marks
   and they share a single row, so there is no per-badge parent to hang it on. */
.cabin-select__badge--showcase {
  color: var(--fog, #a9b0ac);
  background: color-mix(in srgb, var(--fog, #a9b0ac) 10%, transparent);
  border-color: color-mix(in srgb, var(--fog, #a9b0ac) 38%, transparent);
}
.cabin__lede {
  max-width: 46ch;
}
.cabin__tags {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0;
  margin: 1.2rem 0;
}
.cabin__tags li {
  font-family: var(--font-mono);
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--c-muted);
  border: 1px solid color-mix(in srgb, var(--c-muted) 40%, transparent);
  padding: 0.3rem 0.6rem;
}
.cabin__foot {
  display: flex;
  align-items: baseline;
  gap: 1.5rem;
  margin: 1.2rem 0;
  flex-wrap: wrap;
}
.cabin__rate {
  font-size: 0.9rem;
  color: var(--c-muted);
}
.cabin__note {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  color: var(--c-muted);
  margin-top: 0.8rem;
}
.cabin[hidden] {
  display: none;
}
/* Video block on a cabin detail page. Every clip is portrait (shot on a phone
   held upright), so the grid is built from tall cells rather than a 16:9 band —
   aspect-ratio comes from the build-time w/h in cabins.json so nothing reflows
   when a clip loads. */
.cabvid {
  padding-block: clamp(3rem, 9vh, 6rem);
}
/* Fixed column counts rather than auto-fit: with four clips, auto-fit lands on
   three-plus-one at mid widths and leaves a hole in the row. */
.cabvid__grid {
  display: grid;
  grid-template-columns: 1fr;
  align-items: start;
  gap: clamp(0.8rem, 2vw, 1.4rem);
  margin-top: 1.6rem;
}
@media (min-width: 560px) {
  .cabvid__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1000px) {
  .cabvid__grid { grid-template-columns: repeat(4, 1fr); }
}
.cabvid__item {
  margin: 0;
}
.cabvid__video {
  width: 100%;
  height: auto;
  display: block;
  background: var(--norr-black);
  object-fit: cover;
}
.cabvid__cap {
  margin-top: 0.7rem;
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--c-muted);
}
.hosts {
  padding-block: clamp(5rem, 14vh, 10rem) 0;
}
.hosts__copy {
  color: var(--c-muted);
  max-width: 46ch;
  margin-bottom: 1.5rem;
}
.pack {
  padding-block: clamp(5rem, 14vh, 10rem);
}
.pack__copy {
  color: var(--c-muted);
  max-width: 44ch;
  margin-bottom: 2.5rem;
}
@media (max-width: 900px) {
  .cabin__inner,
  .cabin--flip .cabin__inner {
    grid-template-columns: 1fr;
  }
  .cabin--flip .cabin__media {
    order: 0;
  }
}

/* ============ Footer ============ */
/* Paints its own palette so the boundary above it can be a hard cut rather
   than a scroll-linked fade (Alex, 2026-08-13).

   The footer is dark and most pages end light, so it used to be the last
   [data-theme] boundary, and the last one runs to "bottom bottom": the whole
   final screen was spent mid-fade. Measured down the last 1400px of /shop/,
   body text against the root background went

       settled light      14.5:1
       400px from bottom   6.4:1
       200px from bottom   1.8:1   <- unreadable, 4.5:1 is the minimum
       settled dark       16.2:1

   and that stretch is where the facts cards, the newsletter and the wordmark
   all washed out at once. It was the most common scrub on the site: one on six
   of the seven page types.

   With the footer painting itself, [data-theme-cut] on the element skips the
   tween entirely and the cut lands exactly at the footer's own top edge. */
.footer {
  padding-block: clamp(4rem, 10vh, 7rem) 2rem;
  background: var(--norr-black);
  --c-text: var(--birch);
  --c-muted: var(--fog);
  color: var(--c-text);
}
.footer__mark {
  font-family: var(--font-display);
  font-weight: 800;
  letter-spacing: 0.3em;
  font-size: clamp(1.6rem, 4vw, 3rem);
  margin: 0 0 0.2em;
}
.footer__line {
  color: var(--c-muted);
}
.footer__cols {
  display: flex;
  gap: clamp(2rem, 8vw, 8rem);
  margin: 3rem 0;
  flex-wrap: wrap;
}
.footer__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.5rem;
}
.footer__list a {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--c-muted);
}
.footer__list a:hover {
  color: var(--c-text);
}
.footer__list--lang a[aria-current="true"] {
  color: var(--c-text);
}
.footer__fine {
  font-family: var(--font-mono);
  font-size: 0.65rem;
  color: var(--c-muted);
  letter-spacing: 0.08em;
  margin: 0;
}

/* ============ Mobile rhythm ============ */
@media (max-width: 600px) {
  /* Shrink the section token on phones so every token-based section
     (homecat, collection, breather, home band/newsletter) keeps a
     consistent, tighter beat. */
  :root {
    /* Same halving logic as the root value: 9vh was 76px a side and 152px
       gaps on an 844px phone. 7vh gives 59px and 118px. */
    --section-y: clamp(2.75rem, 7vh, 4rem);
  }
  .featured,
  .collection,
  .shop,
  .band,
  .newsletter,
  .chapter,
  .related {
    padding-block: var(--section-y);
  }
}

/* ============ Reduced motion ============ */
@media (prefers-reduced-motion: reduce) {
  html.js .nav {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .card__img,
  .card__title-text,
  .btn {
    transition: none !important;
  }
}

/* ============ Buybox (PDP variant selection) ============ */
/* Visible from the first paint (2026-08-14). It used to stay `visibility:
   hidden` until the Storefront call came back, which reserved its height and
   showed nothing — that blank strip was the "too big" gap between the price and
   the swatches, and it lasted exactly as long as the network took. The label is
   honest before the map arrives: picking a colour is local state now, and the
   line only ever reports what you picked. */
.buybox__picked {
  margin: 1.6rem 0 0;
  color: var(--moss);
  min-height: 1.2em;
}
/* The resolved price inside that line. Bold and in ink against the moss of the
   colour and size beside it, because it is the one part that can change after
   you have already decided: 3XL is €29 where S is €27. */
.buybox__pickedprice {
  font-weight: 700;
  font-size: 1.1em;
  color: var(--ink);
}
button.pdp__dot,
button.pdp__size {
  cursor: pointer;
  appearance: none;
  background-clip: padding-box;
}
button.pdp__dot {
  background: var(--dot);
  transition: box-shadow 0.2s ease, transform 0.2s ease;
}
button.pdp__dot.is-active {
  box-shadow: 0 0 0 2px var(--c-bg), 0 0 0 3.5px var(--moss);
  transform: scale(1.12);
}
button.pdp__size {
  background: transparent;
  transition: border-color 0.2s ease, color 0.2s ease;
}
button.pdp__size.is-active {
  border-color: var(--moss);
  color: var(--moss);
}
button.pdp__size:disabled {
  opacity: 0.35;
  text-decoration: line-through;
  cursor: not-allowed;
}
button.pdp__add:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
/* JS off (or API down): show the Shopify fallback link, hide the app button.
   Scoped to the buybox: [data-add] is a shared hook, not a buybox-only one. */
.buybox button[data-add] { display: none; }
.buybox--ready button[data-add] { display: inline-block; }
.buybox--ready .buybox__fallback { display: none; }
html:not(.js) .buybox__picked { display: none; }

/* ============ Cart drawer ============ */
.cart-overlay {
  position: fixed;
  inset: 0;
  background: color-mix(in srgb, var(--norr-black) 55%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease;
  z-index: 60;
}
.cart-overlay--open {
  opacity: 1;
  pointer-events: auto;
}
.cart {
  position: fixed;
  top: 0;
  right: 0;
  height: 100dvh;
  width: min(420px, 100vw);
  background: var(--snow);
  color: var(--ink);
  z-index: 61;
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  transition: transform 0.45s cubic-bezier(0.32, 0.72, 0, 1);
  padding: 1.4rem 1.6rem calc(1.4rem + env(safe-area-inset-bottom));
}
.cart--open {
  transform: translateX(0);
}
@media (prefers-reduced-motion: reduce) {
  .cart, .cart-overlay { transition: none; }
}
.cart-lock, .cart-lock body {
  overflow: hidden;
}
.cart__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-bottom: 1rem;
  border-bottom: 1px solid color-mix(in srgb, var(--ink) 14%, transparent);
}
.cart__eyebrow {
  color: var(--ink);
  margin: 0;
}
/* 44×44 tap area (WCAG 2.5.8) around a × that still draws at 1.6rem. Measured
   2026-08-14: the button was 27.8×32.
   The negative margins are the whole trick. .cart__head is a space-between flex
   row whose height the button sets, so growing the button to 44 would have
   pushed the rule under the head down 12px and slid the × 8px left off the
   panel's right margin. margin-block: -6px gives back (44-32)/2 top and bottom;
   margin-right: -0.5rem gives back (44-27.8)/2 at the side. Layout box stays
   27.8×32, the glyph does not move, the hit area is 44×44.
   Panel padding is 1.6rem, so the target still stops 17.6px short of the edge. */
.cart__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  margin-block: -6px;
  margin-right: -0.5rem;
  background: none;
  border: none;
  font-size: 1.6rem;
  line-height: 1;
  color: var(--ink);
  cursor: pointer;
  padding: 0;
}
.cart__empty {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: #6b716d;
  margin: auto 0;
  text-align: center;
  line-height: 2;
}
.cart__empty a {
  color: var(--moss);
  border-bottom: 1px solid currentColor;
}
.cart__lines {
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  flex: 1;
}
.cart__line {
  display: flex;
  gap: 1rem;
  padding: 1.1rem 0;
  border-bottom: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
}
.cart__thumb {
  width: 72px;
  height: 72px;
  object-fit: cover;
  background: var(--birch);
  flex-shrink: 0;
}
.cart__line-body {
  flex: 1;
  min-width: 0;
}
.cart__line-title {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.92rem;
  color: var(--ink);
  display: block;
}
.cart__line-variant {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: #6b716d;
  margin: 0.25rem 0 0.6rem;
}
.cart__line-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
.cart__qty {
  display: inline-flex;
  align-items: center;
  border: 1px solid color-mix(in srgb, var(--ink) 20%, transparent);
}
.cart__qty button {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--ink);
  font-size: 0.9rem;
  padding: 0.3rem 0.7rem;
  min-width: 32px;
  min-height: 32px;
}
.cart__qty span {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  min-width: 1.6em;
  text-align: center;
}
.cart__line-price {
  font-family: var(--font-mono);
  font-size: 0.82rem;
}
.cart__remove {
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: #6b716d;
  padding: 0;
  margin-top: 0.55rem;
  border-bottom: 1px solid currentColor;
}
.cart__foot {
  padding-top: 1.1rem;
  border-top: 1px solid color-mix(in srgb, var(--ink) 14%, transparent);
}
.cart__subtotal {
  display: flex;
  justify-content: space-between;
  font-family: var(--font-mono);
  font-size: 0.85rem;
}
.cart__subtotal-label {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.72rem;
  color: #6b716d;
}
.cart__note {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  color: #6b716d;
  margin: 0.5rem 0 1rem;
}
.cart__checkout {
  display: block;
  width: 100%;
  text-align: center;
}

/* ============ Buybox colour preview ============ */
.buybox__preview {
  margin: 1rem 0 0;
  width: min(220px, 100%);
  aspect-ratio: 1;
  background: var(--snow);
  border: 1px solid color-mix(in srgb, var(--c-muted) 35%, transparent);
  overflow: hidden;
}
.buybox__preview img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.35s ease;
}
.buybox__preview img.is-in {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .buybox__preview img { transition: none; }
}

/* ============ Mockup lightbox ============ */
.buybox__preview[role="button"] {
  cursor: zoom-in;
}
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  padding: clamp(1rem, 4vw, 3rem);
  background: color-mix(in srgb, var(--norr-black) 94%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
  cursor: zoom-out;
}
.lightbox--open {
  opacity: 1;
  pointer-events: auto;
}
.lightbox__img {
  max-width: min(90vw, 900px);
  max-height: 80vh;
  object-fit: contain;
  opacity: 0;
  transition: opacity 0.35s ease;
}
.lightbox__img.is-in {
  opacity: 1;
}
.lightbox__label {
  color: var(--fog);
  margin: 0;
}
.lightbox__close {
  position: absolute;
  top: max(1rem, env(safe-area-inset-top));
  right: 1.4rem;
  background: none;
  border: none;
  color: var(--birch);
  font-size: 2.2rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.4rem;
  min-width: 44px;
  min-height: 44px;
}
@media (prefers-reduced-motion: reduce) {
  .lightbox, .lightbox__img { transition: none; }
}
/* Zoomable lightbox frame (gallery PDP) — clips the pan/zoom to a square. */
.lightbox__frame {
  width: min(90vw, 80vh, 900px);
  aspect-ratio: 1 / 1;
  overflow: hidden;
  display: flex;
  touch-action: none;
}
.lightbox__frame .lightbox__img {
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  transform-origin: 0 0;
  cursor: zoom-in;
  user-select: none;
  -webkit-user-select: none;
  -webkit-user-drag: none;
}

/* ============ Cabins map ============ */
.cmap { padding: clamp(40px, 7vh, 96px) 0 clamp(16px, 3vh, 40px); }
/* The half of the hero lede that explains how the collection works. It moved
   here on 2026-08-14: the hero says what this is, this says how it behaves,
   which is the question the map answers anyway. */
.cmap__lede {
  color: var(--c-muted);
  max-width: 46ch;
  margin: 1.1rem 0 0;
}
.cmap__hint {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--c-muted);
  margin-top: 1rem;
}
/* "Hover" is a lie on a phone. Both sentences ship in the markup and the
   pointer decides which one shows — keyed on (hover: none) rather than a
   width, because a wide touchscreen still cannot hover. */
.cmap__hint-tap { display: none; }
@media (hover: none) {
  .cmap__hint-hover { display: none; }
  .cmap__hint-tap { display: inline; }
}
.cmap__stage {
  position: relative;
  /* The map is portrait, so its height cap is usually what binds. Width is
     derived from that cap and the frame's own aspect (set by cabin-map.js, which
     picks a wide or narrow frame) so the box hugs the drawn map — a fixed
     max-width would disagree with the height cap and leave dead space inside
     the frame. The 720px ceiling only stops it sprawling on short, wide windows. */
  max-width: min(720px, calc(76vh * var(--map-aspect, 0.78)));
  margin: clamp(24px, 4vh, 48px) auto 0;
  /* dragging to pan would otherwise sweep a text selection across the city and
     lake labels — the map is a drag surface, nothing in it is worth selecting */
  -webkit-user-select: none;
  user-select: none;
}
.cmap__svg {
  display: block;
  width: 100%;
  height: auto;
  max-height: 76vh;
  touch-action: pan-y; /* keep page scroll; pinch is handled by pointer events */
}
/* Finland is roughly 0.35 wide for every 1 tall, and this section is wide and
   short — so the map can never fill the width on its own without either shrinking
   the country inside a huge frame or running off the bottom of the screen. Past
   900px the intro text moves alongside it instead: the section fills edge to edge,
   and the map grows, because it no longer shares a column with the heading. */
@media (min-width: 900px) {
  .cmap .container {
    display: grid;
    /* minmax(0, 1fr), not auto: the stage's child is width:100%, so an auto
       column and a percentage child size off each other and the map collapses */
    grid-template-columns: minmax(16rem, 26rem) minmax(0, 1fr);
    gap: clamp(2rem, 5vw, 5.5rem);
    align-items: center;
  }
  .cmap__intro { max-width: 26rem; }
  .cmap__stage {
    width: 100%;
    margin: 0 auto;
    /* taller than the stacked layout allowed, since nothing sits above it now */
    max-width: min(760px, calc(84vh * var(--map-aspect, 0.78)));
  }
  .cmap__svg { max-height: 84vh; }
}
.cmap-ctx { fill: none; stroke: rgba(169, 176, 172, 0.2); stroke-width: 0.7; }
/* Figure-ground: land is opaque so the subject separates from context by tone —
   and so the coastal glow underneath survives only on the water side. The hex
   values are today's translucent washes flattened over the page's near-black,
   visually identical to what they replace. */
.cmap-ctxfill { fill: #111514; }
/* international borders between context countries — the classic dashed line.
   Finland's own border keeps the solid drawn outline, the strongest stroke on
   the map, so it is excluded from this layer at the generator. */
.cmap-border {
  fill: none;
  stroke: rgba(169, 176, 172, 0.3);
  stroke-width: 0.6;
  stroke-dasharray: 2 2.6;
}
/* fill/stroke are inherited properties, so setting them on the <use> reaches
   the shadow clone — descendant selectors into the clone would not */
.cmap-glowland use {
  fill: none;
  stroke-linejoin: round;
  stroke-linecap: round;
}
/* widest first (drawn under), tightest last — reads as one soft falloff */
.cmap-glow-3 { stroke: rgba(140, 175, 190, 0.045); stroke-width: 10; }
.cmap-glow-2 { stroke: rgba(140, 175, 190, 0.07); stroke-width: 6; }
.cmap-glow-1 { stroke: rgba(140, 175, 190, 0.1); stroke-width: 3; }
.cmap-fi {
  fill: #1a211e;
  stroke: var(--fog);
  stroke-width: 1.3;
  stroke-linejoin: round;
}
/* rivers share the lakes' water tone — veins, not features; width per river
   comes from the dataset's own stroke weights, baked in by the generator */
.cmap-rivers path {
  fill: none;
  stroke: rgba(150, 170, 175, 0.14);
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* distant lights — dimmer and cooler than the amber cabins, so the hierarchy
   of "lit windows" stays with the cabins */
.cmap-towns circle { fill: rgba(222, 227, 218, 0.34); }
/* national names: FINLAND anchors the subject; neighbours whisper from the
   background, a step fainter than the Finnish region labels */
.cmap-countries text {
  font-family: var(--font-mono);
  /* sized so "Sweden" fits the 40-unit band between the wide and narrow
     frames' left edges — see the generator's COUNTRIES note */
  font-size: 7px;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  fill: rgba(169, 176, 172, 0.12);
}
.cmap-countries .cmap-country--fi {
  font-size: 11px;
  letter-spacing: 0.5em;
  fill: rgba(169, 176, 172, 0.2);
}
.cmap-regions text {
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  fill: rgba(169, 176, 172, 0.16);
}
/* A little more present than the land wash but still far below the warm
   cabin markers — cool reads as water against the #E2B66F "lit window"
   glow without competing with it. fill-rule: evenodd handles Saimaa's and
   Pääjärvi's island holes correctly regardless of the source ring winding. */
.cmap-lake {
  fill: rgba(150, 170, 175, 0.16);
  fill-rule: evenodd;
}
.cmap-lake-label text {
  font-family: var(--font-mono);
  font-size: 6px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  fill: rgba(169, 176, 172, 0.4);
}
.cmap-city circle { fill: rgba(169, 176, 172, 0.55); }
.cmap-city text {
  font-family: var(--font-mono);
  font-size: 7px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  fill: rgba(169, 176, 172, 0.6);
}
.cmap-mk { cursor: pointer; outline: none; }
.cmap-mk__hit { fill: transparent; }
/* each cabin is a lit window in the dark — soft warm halo, warm light pulse */
.cmap-mk__glow {
  opacity: 0.9;
  transform-box: fill-box;
  transform-origin: center;
  transition: opacity 0.35s ease, transform 0.35s ease;
  pointer-events: none;
}
.cmap-mk.is-active .cmap-mk__glow,
.cmap-mk:focus-visible .cmap-mk__glow { opacity: 1; transform: scale(1.3); }
.cmap-mk__window {
  fill: #e2b66f;
  transition: fill 0.2s ease;
  pointer-events: none;
}
.cmap-mk.is-active .cmap-mk__window,
.cmap-mk:focus-visible .cmap-mk__window { fill: #f4dcac; }
.cmap-mk__ring {
  fill: none;
  stroke: rgba(226, 182, 111, 0.85);
  stroke-width: 1.1;
  transform-box: fill-box;
  transform-origin: center;
  animation: cmap-pulse 2.8s ease-out infinite;
  /* the pulse scales to 3.4x (r=5 -> ~17 units), which would otherwise
     transiently swallow clicks meant for a neighbouring marker */
  pointer-events: none;
}
@keyframes cmap-pulse {
  from { transform: scale(1); opacity: 0.5; }
  to { transform: scale(3.4); opacity: 0; }
}
.cmap-mk__aframe {
  fill: var(--norr-black);
  stroke: var(--birch);
  stroke-width: 1.2;
  stroke-linejoin: round;
  transform-box: fill-box;
  transform-origin: center;
  transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), stroke 0.2s ease;
}
.cmap-mk.is-active .cmap-mk__aframe, .cmap-mk:focus-visible .cmap-mk__aframe {
  transform: scale(1.5);
  stroke: var(--snow);
}
.cmap-mk.is-active .cmap-mk__ring { animation-duration: 1.4s; }
/* A shared plot rests as one disc with a count, in the same "lit window in the
   dark" family as .cmap-mk — its own glow, pulse ring and hover/focus treatment,
   sized down from the marker so it doesn't read as just another cabin pin. */
.cmap-cluster { outline: none; cursor: pointer; }
.cmap-cluster-hit { fill: transparent; cursor: pointer; }
.cmap-cluster-glow {
  opacity: 0.9;
  transform-box: fill-box;
  transform-origin: center;
  transition: opacity 0.35s ease, transform 0.35s ease;
  pointer-events: none;
}
.cmap-cluster-ring {
  fill: none;
  stroke: rgba(226, 182, 111, 0.85);
  stroke-width: 1.1;
  transform-box: fill-box;
  transform-origin: center;
  animation: cmap-pulse 2.8s ease-out infinite;
  pointer-events: none;
}
.cmap-cluster-badge {
  fill: var(--norr-black);
  stroke: var(--birch);
  stroke-width: 1.2;
  transition: stroke 0.2s ease;
}
.cmap-cluster-badge-text {
  fill: var(--birch);
  font-family: var(--font-mono);
  font-size: 7px;
  text-anchor: middle;
  dominant-baseline: central;
  pointer-events: none;
}
.cmap-cluster:hover .cmap-cluster-glow,
.cmap-cluster:focus-visible .cmap-cluster-glow { opacity: 1; transform: scale(1.3); }
.cmap-cluster:hover .cmap-cluster-ring,
.cmap-cluster:focus-visible .cmap-cluster-ring { animation-duration: 1.4s; }
.cmap-cluster:hover .cmap-cluster-badge,
.cmap-cluster:focus-visible .cmap-cluster-badge { stroke: var(--snow); stroke-width: 1.4; }
/* Dim cluster discs when all cabins on that plot are filtered out, matching
   the treatment of lone markers so filtered plots don't mislead as potential matches. */
.cmap-cluster { transition: opacity 0.3s ease; }
/* dimming for both discs and markers lives with the filter rules further down */
.cmap-spoke {
  stroke: rgba(226, 182, 111, 0.35);
  stroke-width: 0.6;
  pointer-events: none;
}
/* Joins a flagship's triangle (visible at rest) to its badge, so the pair
   reads as one object rather than two pins with an ambiguous relationship —
   same visual language as .cmap-spoke, but lives inside the badge's own <g>
   so it hides automatically whenever the badge does (cluster expanded). */
.cmap-cluster-connector {
  stroke: rgba(226, 182, 111, 0.35);
  stroke-width: 0.6;
  pointer-events: none;
}
/* SVG elements don't get the browser's built-in [hidden] → display:none rule
   (that's an HTMLElement-only UA style), so each hideable class needs its own. */
.cmap-mk[hidden], .cmap-cluster[hidden], .cmap-spokes[hidden] { display: none; }
.cmap__tip {
  position: absolute;
  width: 216px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0 0 12px;
  background: rgba(12, 17, 16, 0.94);
  border: 1px solid rgba(169, 176, 172, 0.35);
  pointer-events: none;
  text-decoration: none;
  transform: translate(-50%, calc(-100% - 14px));
  opacity: 0;
  transition: opacity 0.25s ease, transform 0.25s ease;
  z-index: 5;
}
.cmap__tip[hidden] { display: none; }
.cmap__tip.is-open { opacity: 1; transform: translate(-50%, calc(-100% - 18px)); }
/* Under the marker instead of over it, for markers too high in the frame to
   have room above. cabin-map.js sets this and moves `top` to the marker's
   bottom edge at the same time. */
.cmap__tip.is-below { transform: translate(-50%, 14px); }
.cmap__tip.is-below.is-open { transform: translate(-50%, 18px); }
.cmap__tip-imgwrap {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2; /* reserves the box before the image arrives */
  overflow: hidden;
  margin-bottom: 8px;
  background: rgba(238, 237, 232, 0.04);
}
.cmap__tip-img { display: block; width: 100%; height: 100%; object-fit: cover; }
.cmap__tip-name { color: var(--birch); font-size: 14px; padding: 0 14px; }
.cmap__tip-meta { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.05em; color: var(--fog); padding: 0 14px; }
/* touch has no hover: the preview card itself is tappable → opens the cabin */
@media (hover: none) and (pointer: coarse) {
  .cmap__tip { pointer-events: auto; cursor: pointer; }
}
.cmap__zoom {
  position: absolute;
  top: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.cmap__zoom button {
  width: 38px;
  height: 38px;
  background: rgba(12, 17, 16, 0.7);
  border: 1px solid rgba(169, 176, 172, 0.35);
  color: var(--fog);
  font-family: var(--font-mono);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  transition: color 0.2s ease, border-color 0.2s ease;
}
.cmap__zoom button:hover { color: var(--birch); border-color: rgba(169, 176, 172, 0.7); }
/* 38px is comfortable under a cursor and short of the 44px a fingertip needs.
   The glyph stays the same size; only the button grows around it. */
@media (hover: none) {
  .cmap__zoom button { width: 44px; height: 44px; }
}
.cmap__stage.is-panning { cursor: grabbing; }
.cmap__stage.is-zoomed .cmap__svg { cursor: grab; touch-action: none; }

/* Desktop lamplight — a soft warm glow that follows the cursor and lifts the
   line-art it passes, "out of the dark, into the light". JS moves it via
   transform; screen blend warms whatever's beneath. */
.cmap__lamp {
  position: absolute;
  top: 0;
  left: 0;
  width: 440px;
  height: 440px;
  margin: -220px 0 0 -220px;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(232, 194, 132, 0.22) 0%,
    rgba(232, 194, 132, 0.08) 38%,
    transparent 70%);
  mix-blend-mode: screen;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.5s ease;
  z-index: 2;
}
.cmap__stage.is-lit .cmap__lamp { opacity: 1; }
@media (hover: none), (prefers-reduced-motion: reduce) {
  .cmap__lamp { display: none; }
}
/* filter dims the non-matching markers so map + card grid stay in step.
   !important is load-bearing here: the draw-on reveal finishes by writing an
   inline opacity: 1 on every marker and disc, which a class selector cannot
   outrank — without it the chips silently dim nothing once the reveal has run. */
.cmap-mk { transition: opacity 0.3s ease; }
.cmap-mk.is-dim, .cmap-cluster.is-dim { opacity: 0.22 !important; }
.cmap-mk.is-dim .cmap-mk__ring { display: none; }

/* cabin cards — index grid + "more cabins" on detail pages */
.cabgrid { padding-bottom: 0; }
.cabincard { text-decoration: none; color: inherit; }
.cabincard .card__media { background: var(--norr-black); }
.cabincard.is-active { outline: 1px solid var(--moss); outline-offset: 4px; }
/* Select mark on the index cards — amber pill on the photo so it's evident
   at a glance when scanning the grid (matches the detail-page badge). */
/* A place can carry both marks (Teijo is Select and not rentable), so the
   badges live in a column rather than each being absolutely placed at the same
   corner, where the second would simply cover the first. */
.cabincard__marks {
  position: absolute;
  top: 0.7rem;
  left: 0.7rem;
  z-index: 2;
  display: flex;
  /* Side by side, matching the detail page. They wrap rather than overflow on
     the narrowest cards: two badges are wider than a card in the phone's
     two-up grid, and a fixed row would push the second one off the photo. */
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 0.3rem;
  /* stay inside the photo, minus the 0.7rem inset on each side */
  max-width: calc(100% - 1.4rem);
}
.cabincard__select {
  font-family: var(--font-mono);
  font-size: 0.56rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: #e2b66f;
  background: rgba(12, 17, 16, 0.82);
  border: 1px solid color-mix(in srgb, #e2b66f 45%, transparent);
  padding: 0.34rem 0.5rem;
}
/* Showcase mark — a cabin that belongs in the collection but takes no bookings.
   Deliberately fog-toned, not amber: Select is an earned recommendation and
   should stay the only warm mark in the grid. */
.cabincard__select--showcase {
  color: var(--fog, #a9b0ac);
  border-color: color-mix(in srgb, var(--fog, #a9b0ac) 45%, transparent);
}
.cabmore {
  padding-block: clamp(3rem, 8vh, 6rem);
  border-top: 1px solid rgba(169, 176, 172, 0.15);
}

/* "← All cabins" back link (detail pages) */
/* top link must clear the fixed nav (z-50) — otherwise the nav swallows the
   click and it crowds the wordmark; the bottom --center link needs no offset */
.cabin-back-wrap { padding-top: clamp(6rem, 13vh, 8.5rem); }
.cabin-back-wrap--center { text-align: center; padding-top: clamp(2rem, 5vh, 3.5rem); }
.cabin-back {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--c-muted);
  transition: color 0.25s ease;
}
.cabin-back:hover { color: var(--c-text); }
.cabin-back__arrow { display: inline-block; transition: transform 0.25s ease; }
.cabin-back:hover .cabin-back__arrow { transform: translateX(-4px); }

@media (max-width: 900px) {
  .cmap__stage { max-width: 380px; }
  .cmap__svg { max-height: 62vh; }
}
@media (prefers-reduced-motion: reduce) {
  .cmap-mk__ring, .cmap-cluster-ring { animation: none; opacity: 0; }
  .cmap-mk, .cmap-mk__aframe, .cmap-mk__glow, .cmap-mk__window,
  .cmap__tip, .cabin-back__arrow,
  .cmap-cluster, .cmap-cluster-glow, .cmap-cluster-badge { transition: none; }
}

/* ============ Shop by type: worlds, shelves, ghost cards, design filter ==== */
.typenav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 3rem 0 0;
}
.typenav a {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  padding: 0.55rem 1rem;
  border: 1px solid color-mix(in srgb, var(--c-muted) 55%, transparent);
  color: var(--c-text);
  transition: border-color 0.2s ease, color 0.2s ease;
}
.typenav a:hover { border-color: var(--moss); color: var(--moss); }

.world__rule {
  border-top: 1px solid color-mix(in srgb, var(--c-muted) 40%, transparent);
  padding-top: 1.4rem;
  margin-top: clamp(3rem, 7vh, 5rem);
}
.world__title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(2rem, 5vw, 3.6rem);
  line-height: 0.98;
  margin: 0.3rem 0 0.6rem;
}
.world__lede { color: var(--c-muted); max-width: 52ch; }

.shelf { padding: clamp(2rem, 5vh, 3.2rem) 0 0.6rem; }
.shelf__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.2rem;
}
.shelf__title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.3rem, 3vw, 1.9rem);
  margin: 0;
}
.shelf__count {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--c-muted);
}
/* On a phone the space-between row squeezes both children until the title AND
   the count each wrap to two lines and interleave. Measured at 390px: "Words
   for the Woods" and "Cabins & Landscapes" both did it, and so did the
   favourites shelf on /shop/. Found because the dark shelf divider made it
   obvious, fixed everywhere because it was never only there. */
@media (max-width: 700px) {
  .shelf__head {
    display: block;
  }
  .shelf__count {
    display: block;
    margin-top: 0.5rem;
  }
}

.designfilter {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 2rem;
}
.designfilter button {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  padding: 0.5rem 0.95rem;
  border: 1px solid color-mix(in srgb, var(--c-muted) 55%, transparent);
  border-radius: 999px;
  background: transparent;
  color: var(--c-text);
  cursor: pointer;
  transition: border-color 0.2s ease, color 0.2s ease;
}
.designfilter button:hover { border-color: var(--moss); color: var(--moss); }
.designfilter button.is-on { border-color: var(--moss); color: var(--moss); }
.grid .card[hidden] { display: none; }

.ghostgrid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 2.5rem var(--gutter);
}
.ghostcard { display: block; }
.ghostcard__media {
  aspect-ratio: 4 / 5;
  border: 1px dashed color-mix(in srgb, var(--c-muted) 55%, transparent);
  border-radius: 2px;
  display: grid;
  place-items: center;
  position: relative;
  background: color-mix(in srgb, var(--snow) 45%, transparent);
}
.ghostcard__media--photo { border-style: solid; overflow: hidden; }
.ghostcard__media--photo :is(img, picture) {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 30%;
}
.ghostcard__icon { font-size: 1.8rem; opacity: 0.45; }
.ghostcard__soon {
  position: absolute;
  top: 0.7rem;
  right: 0.7rem;
  z-index: 1;
  font-family: var(--font-mono);
  font-size: 0.58rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  padding: 0.3rem 0.6rem;
  border: 1px solid color-mix(in srgb, var(--c-muted) 60%, transparent);
  border-radius: 999px;
  color: var(--c-muted);
  background: color-mix(in srgb, var(--snow) 80%, transparent);
}
.ghostcard__meta { margin-top: 0.8rem; }
.ghostcard__title { font-family: var(--font-display); font-weight: 800; font-size: 1rem; margin: 0; }
.ghostcard__sub { color: var(--c-muted); font-size: 0.82rem; margin: 0.25rem 0 0; }

@media (max-width: 880px) {
  .ghostgrid { grid-template-columns: repeat(2, 1fr); gap: 1.6rem 1rem; }
}

/* ============ Homepage category strip (Wear · Layer · Home · Stay) ======== */
.homecat {
  padding-block: var(--section-y) 0;
}
.homecat__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem;
  margin-bottom: 2.2rem;
}
.homecat__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}
.homecat-card {
  display: block;
  border-radius: 2px; /* match the subtle rounding on the other image cards */
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
  background: var(--snow);
  box-shadow: 0 24px 50px -32px rgba(12, 17, 16, 0.35);
  color: var(--ink);
}
.homecat-card__media {
  position: relative;
  aspect-ratio: 1 / 1;
  overflow: hidden;
}
.homecat-card__media :is(img, picture) {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.9s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.homecat-card__media--goods {
  background:
    radial-gradient(circle at 68% 34%, rgba(226, 182, 111, 0.22), transparent 58%),
    linear-gradient(150deg, #33302a, #1a1815 80%);
}
.homecat-card__media::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(4, 9, 8, 0.28), transparent 34%, transparent 70%, rgba(4, 9, 8, 0.42));
}
.homecat-card__label {
  position: absolute;
  top: 0.85rem;
  left: 0.95rem;
  z-index: 1;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: color-mix(in srgb, var(--birch) 72%, transparent);
}
.homecat-card__meta {
  /* Stacked, not side-by-side — a pill next to the text squeezes the copy
     into one-word lines on quarter-width cards. */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.7rem;
  padding: 0.95rem 1rem 1.05rem;
}
.homecat-card__title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.15rem;
  line-height: 1.05;
  margin: 0;
}
.homecat-card__sub {
  color: var(--c-muted);
  font-size: 0.78rem;
  line-height: 1.45;
  margin: 0.3rem 0 0;
}
.homecat-card__pill {
  display: inline-flex;
  align-items: center;
  height: 1.9rem;
  padding: 0 0.7rem;
  border: 1px solid color-mix(in srgb, var(--ink) 22%, transparent);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  white-space: nowrap;
  color: color-mix(in srgb, var(--ink) 78%, transparent);
}
.homecat-card:hover .homecat-card__media :is(img) { transform: scale(1.05); }
@media (max-width: 880px) {
  .homecat__head { flex-direction: column; align-items: flex-start; gap: 0.8rem; }
  /* Mobile: a swipe rail instead of a cramped grid. Cards run large, the
     next one peeks past the edge to signal "scroll for more". Bleeds to the
     screen edges by cancelling the container's side padding. */
  .homecat__grid {
    display: flex;
    grid-template-columns: none;
    gap: 0.9rem;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    margin-inline: calc(var(--gutter) * -1);
    padding-inline: var(--gutter);
    scroll-padding-inline: var(--gutter);
    scrollbar-width: none;
  }
  .homecat__grid::-webkit-scrollbar { display: none; }
  .homecat-card {
    flex: 0 0 78%;
    scroll-snap-align: start;
  }
  .homecat-card__media { aspect-ratio: 4 / 5; }
}

/* ============ Shop entrance hero (atmospheric showcase) ============ */
.shop-hero {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  /* Fill the first screen so the light grid stays below the fold — otherwise
     the dawn transition (triggered when the grid enters the viewport bottom)
     starts before any scroll and washes the hero toward grey. */
  min-height: 100vh;
  display: flex;
  /* Bottom-aligned, matching the homepage hero. It used to be top-aligned to
     stop an empty upper band going muddy during the dawn transition, but that
     was when the hero was the animated aurora; it is a photograph now, so the
     reasoning is spent. Dropping the type to the foot of the frame also clears
     the figures, which is what the buttons used to be crowding. */
  align-items: flex-end;
  padding-top: clamp(6rem, 16vh, 11rem);
  padding-bottom: clamp(3rem, 8vh, 6rem);
}
/* Photograph behind the type, same treatment as the homepage hero — it reuses
   .hero__img and .hero__shade so the zoom-out and the scrim stay defined in
   one place and the two pages can't drift apart. */
.shop-hero__media {
  position: absolute;
  inset: 0;
  z-index: 0;
}
/* Was a two-column grid holding the intro beside three cards. The cards are
   gone, so the intro gets the hero to itself and sits in the left column
   rather than being stretched across the full width. */
.shop-hero__grid {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 46rem);
  align-items: end;
  width: 100%;
}
/* 36rem, not 42ch: the two buttons need ~35rem side by side and were wrapping
   onto separate lines once the lede shrank to one sentence. */
.shop-hero__intro { max-width: 36rem; }
.shop-hero__title {
  font-size: clamp(3.2rem, 8vw, 7rem);
  line-height: 0.92;
  margin: 0.4rem 0 0;
}
.shop-hero__lede {
  color: var(--c-muted);
  max-width: 42ch;
  font-size: clamp(1rem, 1.6vw, 1.12rem);
  line-height: 1.7;
  margin-top: 1.4rem;
}
.shop-hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.8rem;
  margin-top: 1.8rem;
}

/* ghost button (dark hero) */
.btn--ghost {
  background: transparent;
  color: var(--birch);
  border: 1px solid color-mix(in srgb, var(--birch) 38%, transparent);
  transition: background 0.3s ease, border-color 0.3s ease;
}
.btn--ghost:hover {
  background: color-mix(in srgb, var(--birch) 10%, transparent);
  border-color: var(--birch);
}

/* micro stats */
.shop-micro {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.75rem;
  margin: 2.4rem 0 0;
  max-width: 34rem;
}
.shop-micro__item {
  padding-top: 0.85rem;
  border-top: 1px solid color-mix(in srgb, var(--birch) 18%, transparent);
}
.shop-micro dt {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.1rem;
  color: var(--c-text);
  margin-bottom: 0.35rem;
}
.shop-micro dd {
  margin: 0;
  color: var(--c-muted);
  font-size: 0.8rem;
  line-height: 1.45;
}

/* showcase stage */
.shop-showcase {
  position: relative;
  min-height: 34rem;
}
.shop-feature {
  position: absolute;
  inset: 1rem 20% 2.5rem 0;
  border-radius: 10px;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--birch) 14%, transparent);
  box-shadow: 0 40px 90px -40px rgba(0, 0, 0, 0.75);
}
.shop-feature__link { display: block; height: 100%; text-decoration: none; color: var(--birch); }
.shop-feature__media { position: absolute; inset: 0; }
.shop-feature__media :is(img, picture) { width: 100%; height: 100%; object-fit: cover; display: block; }
.shop-feature::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(4, 9, 8, 0.05), rgba(4, 9, 8, 0.34) 52%, rgba(4, 9, 8, 0.9));
}
.shop-feature__copy {
  position: absolute;
  inset: auto 40% 1.1rem 1.1rem; /* keep copy in the left half — the tiles overlap the right */
  z-index: 1;
  display: flex;
  align-items: flex-end;
  gap: 0.8rem;
}
.shop-feature__title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.6rem, 2.4vw, 2.3rem);
  line-height: 0.98;
  margin: 0;
  max-width: 12ch;
}
.label {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: color-mix(in srgb, var(--birch) 62%, transparent);
  margin: 0 0 0.4rem;
}
.price-pill,
.soon-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2.1rem;
  padding: 0 0.75rem;
  border: 1px solid color-mix(in srgb, var(--birch) 24%, transparent);
  border-radius: 999px;
  background: rgba(4, 9, 8, 0.5);
  color: var(--birch);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  white-space: nowrap;
  backdrop-filter: blur(10px);
}

/* floating teaser tiles (glass) — now links into the goods shelves */
.shop-tile {
  position: absolute;
  width: min(15rem, 46%);
  border-radius: 10px;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--birch) 14%, transparent);
  background: color-mix(in srgb, var(--norr-black) 40%, transparent);
  box-shadow: 0 30px 70px -34px rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(14px);
  display: block;
  color: inherit;
  text-decoration: none;
}
.shop-tile--a { top: 0; right: 0; animation: shop-drift 7s ease-in-out infinite alternate; }
.shop-tile--b { right: 6%; bottom: 0; animation: shop-drift 7s ease-in-out -2.6s infinite alternate; }
.shop-tile__visual {
  position: relative;
  min-height: 8.5rem;
  overflow: hidden;
  background-size: cover;
  background-position: center;
}
.shop-tile__visual img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 28%;
  display: block;
}
.shop-tile__visual--hoodie {
  background:
    radial-gradient(circle at 32% 30%, rgba(155, 183, 155, 0.28), transparent 60%),
    linear-gradient(150deg, #2c3733, #171d1b 78%);
}
.shop-tile__visual--goods {
  background:
    radial-gradient(circle at 68% 34%, rgba(226, 182, 111, 0.22), transparent 58%),
    linear-gradient(150deg, #33302a, #1a1815 80%);
}
.shop-tile__info {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 0.7rem;
  padding: 0.85rem 0.9rem;
}
.shop-tile__title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.2rem;
  margin: 0;
  color: var(--birch);
}
.shop-tile__note {
  margin: 0.3rem 0 0;
  color: var(--c-muted);
  font-size: 0.74rem;
  line-height: 1.4;
}
.soon-pill {
  height: 1.7rem;
  padding: 0 0.65rem;
  font-size: 0.6rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
}

@keyframes shop-drift {
  from { transform: translateY(-6px); }
  to { transform: translateY(7px); }
}

/* Premium hover — a soft aurora-tinted light rakes diagonally across the panel
   once (a single pass, decelerating), the media eases in, edges brighten. No
   cursor tracking. Transitions live in the :hover state so it only animates on
   enter and snaps back invisibly on leave (no reverse sweep). */
.shop-feature,
.shop-tile,
.homecat-card {
  isolation: isolate;
  transition: box-shadow 0.45s ease, border-color 0.45s ease,
    transform 0.5s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.shop-feature::before,
.shop-tile::before,
.homecat-card::before {
  content: "";
  position: absolute;
  inset: -40% -14%;
  z-index: 4;
  pointer-events: none;
  background: linear-gradient(
    114deg,
    transparent 40%,
    rgba(150, 183, 160, 0.08) 46%,
    rgba(226, 236, 216, 0.34) 50%,
    rgba(110, 170, 164, 0.10) 54%,
    transparent 60%
  );
  mix-blend-mode: screen;
  opacity: 0;
  transform: translateX(-165%);
}
.shop-feature:hover::before,
.shop-tile:hover::before,
.shop-feature:focus-within::before,
.homecat-card:hover::before,
.homecat-card:focus-visible::before {
  opacity: 1;
  transform: translateX(165%);
  transition: transform 1s cubic-bezier(0.22, 0.61, 0.36, 1), opacity 0.35s ease;
}
.shop-feature__media img,
.shop-tile__visual {
  transition: transform 0.9s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.shop-feature:hover .shop-feature__media img,
.shop-feature:focus-within .shop-feature__media img { transform: scale(1.045); }
.shop-tile:hover .shop-tile__visual { transform: scale(1.06); }
.shop-feature:hover,
.shop-tile:hover,
.shop-feature:focus-within,
.homecat-card:hover,
.homecat-card:focus-visible {
  border-color: color-mix(in srgb, var(--birch) 30%, transparent);
  box-shadow: 0 46px 100px -38px rgba(0, 0, 0, 0.82);
}
.shop-feature:hover,
.shop-feature:focus-within,
.homecat-card:hover,
.homecat-card:focus-visible { transform: translateY(-5px); }
@media (prefers-reduced-motion: reduce) {
  .shop-feature::before,
  .shop-tile::before,
  .homecat-card::before { display: none; }
  .shop-feature:hover .shop-feature__media img,
  .shop-tile:hover .shop-tile__visual,
  .shop-feature:hover,
  .homecat-card:hover { transform: none; }
}

@media (max-width: 880px) {
  /* Was `min-height: 0` — right when the hero was a text column stacked above
     three cards and sizing to content was the point. Now it's a photograph,
     so it needs the screen: 88svh rather than the homepage's 100 leaves the
     top of the collections showing, which invites the scroll the removed
     "Scroll" cue used to ask for. svh, not vh, so mobile browser chrome
     doesn't push it into overflow. */
  .shop-hero { min-height: 88svh; }
  .shop-hero__grid { grid-template-columns: 1fr; gap: 2.6rem; }
  .shop-showcase {
    min-height: 0;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.8rem;
  }
  .shop-feature {
    position: relative;
    inset: auto;
    grid-column: 1 / -1;
    aspect-ratio: 4 / 3;
  }
  .shop-tile { position: relative; width: auto; inset: auto; animation: none; }
}
@media (max-width: 480px) {
  .shop-micro { grid-template-columns: 1fr; gap: 0.3rem; }
}
@media (prefers-reduced-motion: reduce) {
  .shop-tile { animation: none !important; }
}

/* ---------- Before / After comparison slider ---------- */
.ba-section { padding: clamp(4rem, 10vw, 8rem) 0 0; }
.ba-section .section-title { margin-bottom: clamp(1.4rem, 4vw, 2.4rem); }

.ba {
  position: relative;
  width: 100%;
  max-width: 960px;
  margin-inline: auto;
  aspect-ratio: 3 / 2;
  border-radius: 16px;
  overflow: hidden;
  user-select: none;
  -webkit-user-select: none;
  touch-action: pan-y;
  box-shadow: 0 30px 80px -34px rgba(0, 0, 0, 0.65);
}
.ba__img { position: absolute; inset: 0; }
.ba__img img,
.ba__img picture { display: block; width: 100%; height: 100%; }
.ba__img img { object-fit: cover; }
/* Top layer (summer) is clipped from the right so it shows left→handle. */
.ba__img--before { clip-path: inset(0 calc(100% - var(--pos, 50%)) 0 0); }

.ba__label {
  position: absolute;
  bottom: 14px;
  z-index: 3;
  padding: 6px 14px;
  border-radius: 999px;
  font: 500 0.78rem/1 var(--font-mono);
  letter-spacing: 0.06em;
  color: var(--snow);
  background: rgba(12, 17, 16, 0.5);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.18);
  pointer-events: none;
}
.ba__label--before { left: 14px; }
.ba__label--after { right: 14px; }

.ba__handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--pos, 50%);
  width: 48px;
  transform: translateX(-50%);
  z-index: 4;
  cursor: ew-resize;
  touch-action: none;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  padding: 0;
}
.ba__handle:focus { outline: none; }
.ba__handle:focus-visible .ba__handle-grip {
  box-shadow: 0 0 0 3px rgba(85, 101, 79, 0.95), 0 2px 14px rgba(0, 0, 0, 0.3);
}
.ba__handle-line {
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 2px;
  transform: translateX(-50%);
  background: rgba(255, 255, 255, 0.9);
}
.ba__handle-grip {
  position: relative;
  z-index: 1;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.96);
  color: var(--norr-black);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 14px rgba(0, 0, 0, 0.3);
}
.ba__handle-grip svg { display: block; margin: 0 -3px; }

.ba__hint {
  text-align: center;
  margin-top: 1rem;
  font: 500 0.72rem/1 var(--font-mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0.55;
}

/* One-time nudge to signal the handle is draggable. */
@media (prefers-reduced-motion: no-preference) {
  .ba__handle-grip { animation: baHint 2.6s ease-in-out 0.9s 2; }
  @keyframes baHint {
    0%, 100% { transform: translateX(0); }
    28% { transform: translateX(-7px); }
    62% { transform: translateX(7px); }
  }
}
.ba.is-dragging .ba__handle-grip { animation: none; }

/* ============ Games guide (cabins pillar) ============ */
.games-hero { padding-bottom: clamp(2.5rem, 6vh, 4.5rem); }
.games-hero__lede {
  color: var(--c-muted);
  max-width: 52ch;
  margin: 0;
}
/* ---- Games, in the works (2026-08-14) ----
   Shown instead of the grid while the guide is unfinished: the three moods it
   is grouped by, numbered, so the page states its shape rather than its
   emptiness. Everything here is type on the hero's own dark ground — no cards,
   because a card implies something to open. */
.games-hero__flag {
  color: var(--fog);
  margin-bottom: 1.1rem;
}
/* The status marker. It replaces an eyebrow that sat in fog grey at eyebrow
   size, which is the same treatment every category label on the site gets, so
   it read as the section's name. This one is a ruled label rather than a
   pill: hairline box, mono, tracked, birch on the photograph's own dark, with
   a small square holding the left edge. It is the first element in the hero
   block, so on a phone it is the first thing under the nav and the state is
   known before a word of the paragraph is read. Deliberately not gold: gold
   means Norrwild Select on this site and borrowing it here would blur both. */
.games-flag {
  display: inline-flex;
  align-items: center;
  gap: 0.7em;
  margin: 0 0 1.4rem;
  padding: 0.62em 1.05em;
  font-family: var(--font-mono, "JetBrains Mono", monospace);
  font-size: 0.76rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--birch);
  border: 1px solid color-mix(in srgb, var(--birch) 58%, transparent);
  background: color-mix(in srgb, var(--norr-black) 62%, transparent);
  backdrop-filter: blur(2px);
}
.games-flag__dot {
  width: 0.5em;
  height: 0.5em;
  background: var(--birch);
  flex: none;
}
.games-soon__eyebrow { margin: 0 0 0.9rem; }
/* The dropdown entry sat level with Cabins and Story, which set the wrong
   expectation before the click. Quiet on purpose: the nav is not the place to
   shout, it only has to stop the item reading as finished. */
.nav__soon {
  font-size: 0.68em;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--c-muted);
  white-space: nowrap;
}
.games-handoff__eyebrow {
  margin: 0 0 0.5rem;
  flex-basis: 100%;
}
/* The ask sits between the preview and the handoff, so the page offers a way
   to be told before it offers somewhere else to go. */
.games-tell { padding-block: clamp(3rem, 8vh, 5.5rem); }
/* No max-width here: this element also carries .container, whose auto margins
   turn any narrower max-width into a centred column. That indented the whole
   block 352px while every other section on the page started at the gutter.
   The copy is held to its measure on .games-tell__copy instead. */
.games-tell__title {
  font-size: clamp(1.7rem, 4vw, 2.6rem);
  margin: 0 0 0.7rem;
}
.games-tell__copy {
  color: var(--c-muted);
  max-width: 46ch;
  margin: 0 0 1.6rem;
}
.games-soon { padding-block: clamp(3.5rem, 10vh, 7rem); }
.games-soon__head {
  display: grid;
  gap: clamp(1rem, 4vw, 3rem);
  align-items: end;
  margin-bottom: clamp(2.5rem, 7vh, 4.5rem);
}
.games-soon__blurb {
  color: var(--c-muted);
  max-width: 42ch;
  margin: 0;
}
.games-soon__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0;
  border-top: 1px solid color-mix(in srgb, var(--birch) 22%, transparent);
}
.games-soon__item {
  padding: 1.6rem 0;
  border-bottom: 1px solid color-mix(in srgb, var(--birch) 22%, transparent);
}
.games-soon__n {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  color: var(--c-muted);
  margin-bottom: 1.4rem;
}
.games-soon__title {
  font-size: clamp(1.5rem, 3.2vw, 2.1rem);
  margin: 0 0 0.5rem;
}
.games-soon__line {
  color: var(--c-muted);
  max-width: 30ch;
  margin: 0;
}
/* The handoff to the thing that IS ready. A birch band that paints itself,
   deliberately NOT a [data-theme] section (Alex, 2026-08-14).

   As a themed section it made the page's only dark→light boundary, and the
   dawn scrub interpolates the whole :root palette across the approach to it.
   On a page this short that fade covers the three moods you are meant to be
   reading: they spent their whole time on screen in mid-grey, halfway between
   the two palettes. Same complaint as the shop hero's muddy transition zone.

   Outside the theme system there is no boundary and nothing to scrub, so the
   page stays dark from the hero to the end of the moods and this band is a
   hard cut. It sets its own palette vars so its type is ink on birch whatever
   :root is doing — the same device .shop2-shelfhead--band uses inverted, and
   the footer's data-theme-cut solves the same problem one section later. */
.games-handoff {
  padding-block: clamp(3rem, 8vh, 5rem);
  background: var(--birch);
  --c-text: var(--ink);
  --c-muted: color-mix(in srgb, var(--ink) 62%, transparent);
  color: var(--c-text);
}
.games-handoff__inner {
  display: flex;
  flex-wrap: wrap;
  gap: 1.2rem 2rem;
  align-items: baseline;
  justify-content: space-between;
}
.games-handoff__title {
  font-size: clamp(1.7rem, 4vw, 2.6rem);
  margin: 0;
  max-width: 16ch;
}
@media (min-width: 800px) {
  .games-soon__head { grid-template-columns: 1.1fr 1fr; }
  .games-soon__list {
    grid-template-columns: repeat(3, 1fr);
    border-bottom: 1px solid color-mix(in srgb, var(--birch) 22%, transparent);
  }
  .games-soon__item {
    border-bottom: 0;
    border-left: 1px solid color-mix(in srgb, var(--birch) 22%, transparent);
    padding: 2rem 1.6rem 2.6rem;
  }
  .games-soon__item:first-child { border-left: 0; padding-left: 0; }
}

.games-group { padding-block: clamp(3rem, 8vh, 5.5rem); }
.games-group__blurb {
  color: var(--c-muted);
  max-width: 46ch;
  margin: 0 0 2.5rem;
}
.games-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gutter);
}
@media (max-width: 1100px) {
  .games-grid { grid-template-columns: repeat(2, 1fr); }
}
/* Mobile: keep two cards per row (compact type) — one-per-row made the
   15-game page a very long scroll. */
@media (max-width: 700px) {
  .games-grid { gap: 1.4rem 0.8rem; }
  .gamecard { padding: 0.9rem 0.9rem 0.85rem; gap: 0.5rem; }
  .gamecard__media { margin: -0.9rem -0.9rem 0.4rem; }
  .gamecard__top { font-size: 0.6rem; }
  .gamecard__name { font-size: 1.05rem; }
  .gamecard__desc { font-size: 0.82rem; }
  .gamecard__links { gap: 0.4rem 1rem; padding-top: 0.35rem; }
  .gamecard__buy, .gamecard__pdf { font-size: 0.62rem; }
}
.gamecard {
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
  padding: 1.5rem 1.5rem 1.4rem;
  border: 1px solid color-mix(in srgb, var(--c-muted) 35%, transparent);
  border-radius: 2px;
  background: color-mix(in srgb, var(--c-text) 3%, transparent);
  transition: transform 0.5s ease, border-color 0.5s ease;
}
@media (hover: hover) and (pointer: fine) {
  .gamecard:hover {
    transform: translateY(-4px);
    border-color: color-mix(in srgb, var(--c-muted) 60%, transparent);
  }
}
.gamecard__top {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.3rem 1rem;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--c-muted);
}
.gamecard__name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.35rem;
  line-height: 1.15;
  margin: 0;
}
.gamecard__desc {
  color: var(--c-muted);
  font-size: 0.95rem;
  margin: 0;
  flex-grow: 1;
}
.gamecard__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 1.5rem;
  padding-top: 0.5rem;
}
.gamecard__buy,
.gamecard__pdf {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding-bottom: 0.2em;
  transition: color 0.25s ease;
}
.gamecard__buy {
  color: var(--c-text);
  border-bottom: 1px solid currentColor;
}
.gamecard__buy:hover { color: var(--c-muted); }
.gamecard__pdf {
  color: var(--c-muted);
  border-bottom: 1px dotted currentColor;
}
.gamecard__pdf:hover { color: var(--c-text); }
.games-disclosure { padding-block: 0.5rem clamp(3rem, 8vh, 5rem); }
.games-disclosure__text {
  color: var(--c-muted);
  font-size: 0.8rem;
  max-width: 60ch;
  margin: 0;
}
.games-teaser { padding-block: clamp(4rem, 10vh, 7rem) 0; }
.games-teaser__copy {
  color: var(--c-muted);
  max-width: 46ch;
  margin-bottom: 1.5rem;
}
@media (prefers-reduced-motion: reduce) {
  .gamecard { transition: none; }
  .gamecard:hover { transform: none; }
}

/* ============ One Candle (games sub-page) ============ */
.oc-hero { padding-bottom: clamp(2rem, 5vh, 3.5rem); }
.oc-hero .cabin-back-wrap { padding-bottom: clamp(2rem, 5vh, 3.5rem); }
.oc-hero__lede { color: var(--c-muted); max-width: 54ch; margin: 0; }
.oc-ritual { padding-block: clamp(2.5rem, 7vh, 5rem); }
.oc-ritual__rules {
  list-style: none;
  margin: 0 0 2.5rem;
  padding: 0;
  max-width: 60ch;
  display: grid;
  gap: 0.9rem;
}
.oc-ritual__rules li {
  position: relative;
  padding-left: 1.6rem;
  color: var(--c-text);
}
.oc-ritual__rules li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.6em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--moss);
}
.oc-ritual__close {
  border-top: 1px solid color-mix(in srgb, var(--c-muted) 30%, transparent);
  padding-top: 1.5rem;
  max-width: 60ch;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.3rem, 3vw, 1.9rem);
  line-height: 1.2;
}
.oc-ritual__close .eyebrow { display: block; margin-bottom: 0.6rem; }
.oc-editions { padding-block: clamp(2.5rem, 7vh, 5rem) clamp(4rem, 10vh, 7rem); }
.oc-editions__grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--gutter);
}
@media (max-width: 700px) { .oc-editions__grid { grid-template-columns: 1fr; } }
.oc-edition {
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
  padding: 1.75rem 1.75rem 1.6rem;
  border: 1px solid color-mix(in srgb, var(--c-muted) 35%, transparent);
  border-radius: 2px;
  background: color-mix(in srgb, var(--c-text) 3%, transparent);
  transition: transform 0.5s ease, border-color 0.5s ease;
}
@media (hover: hover) and (pointer: fine) {
  .oc-edition:hover {
    transform: translateY(-4px);
    border-color: color-mix(in srgb, var(--c-muted) 60%, transparent);
  }
}
.oc-edition__label {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.4rem;
  line-height: 1.15;
  margin: 0;
}
.oc-edition__blurb { color: var(--c-muted); font-size: 0.95rem; margin: 0; flex-grow: 1; }
.oc-edition__get {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--c-muted);
  border-bottom: 1px dotted currentColor;
  padding-bottom: 0.2em;
  align-self: flex-start;
  transition: color 0.25s ease;
}
.oc-edition__get:hover { color: var(--c-text); }
@media (prefers-reduced-motion: reduce) {
  .oc-edition { transition: none; }
  .oc-edition:hover { transform: none; }
}

/* Game card photos (optional per game — full-bleed tile at the card top) */
.gamecard { overflow: hidden; }
.gamecard__media {
  margin: -1.5rem -1.5rem 0.6rem;
  aspect-ratio: 4 / 5;
  background: var(--birch);
  overflow: hidden;
}
.gamecard__media img,
.gamecard__media picture {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
@media (hover: hover) and (pointer: fine) {
  .gamecard__media img { transition: transform 0.6s ease; }
  .gamecard--has-image:hover .gamecard__media img { transform: scale(1.03); }
}
@media (prefers-reduced-motion: reduce) {
  .gamecard__media img { transition: none; }
  .gamecard--has-image:hover .gamecard__media img { transform: none; }
}

/* Games teaser thumbnail strip (cabins page) — whole strip links to the guide */
.games-teaser__strip {
  display: flex;
  gap: 0.6rem;
  margin: 1.75rem 0 1.6rem;
  max-width: max-content;
  transition: transform 0.5s ease;
}
@media (hover: hover) and (pointer: fine) {
  .games-teaser__strip:hover { transform: translateY(-4px); }
}
.games-teaser__thumb {
  width: clamp(56px, 11vw, 110px);
  aspect-ratio: 4 / 5;
  border-radius: 2px;
  overflow: hidden;
  background: var(--snow);
  flex-shrink: 0;
}
/* Five tiles must fit inside the gutter on a 375px phone */
@media (max-width: 480px) {
  .games-teaser__strip { gap: 0.45rem; }
  .games-teaser__thumb { width: clamp(52px, 14.5vw, 110px); }
  .games-teaser__more-n { font-size: 1rem; }
  .games-teaser__more-t { font-size: 0.52rem; }
}
.games-teaser__thumb img,
.games-teaser__thumb picture {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.games-teaser__more {
  display: grid;
  place-content: center;
  gap: 0.15rem;
  text-align: center;
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--c-muted) 35%, transparent);
}
.games-teaser__more-n {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.2rem;
  color: var(--c-text);
}
.games-teaser__more-t {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--c-muted);
}
@media (prefers-reduced-motion: reduce) {
  .games-teaser__strip { transition: none; }
  .games-teaser__strip:hover { transform: none; }
}

/* Mobile: clamp long game descriptions to 4 lines with a More/Less toggle.
   Desktop never clamps; the toggle button only exists below 700px. */
.gamecard__toggle { display: none; }
@media (max-width: 700px) {
  .gamecard__desc {
    display: -webkit-box;
    -webkit-line-clamp: 4;
    -webkit-box-orient: vertical;
    overflow: hidden;
    flex-grow: 0; /* grid-stretched cards must not stretch the clamped box open */
  }
  .gamecard.is-open .gamecard__desc {
    display: block;
    -webkit-line-clamp: unset;
    overflow: visible;
  }
  .gamecard__toggle {
    display: inline-block;
    align-self: flex-start;
    background: none;
    border: none;
    padding: 0 0 0.1em;
    cursor: pointer;
    font-family: var(--font-mono);
    font-size: 0.6rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--c-muted);
    border-bottom: 1px dotted currentColor;
  }
}

/* Games hero — cinematic photo variant (dark image, text on the dark left) */
.games-hero--photo {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  min-height: clamp(460px, 66vh, 700px);
  display: flex;
  flex-direction: column;
  /* flex-end, not space-between: the back link that used to hold the top of
     this column was removed on 2026-08-14, and space-between with a single
     child collapses to flex-start — which would throw the headline up under
     the nav instead of leaving it seated at the foot of the frame. */
  justify-content: flex-end;
  padding-top: clamp(6.5rem, 13vh, 9rem);
  padding-bottom: clamp(2.5rem, 7vh, 5rem);
}
.games-hero--photo .story-hero__media { position: absolute; inset: 0; z-index: 0; overflow: hidden; background: var(--norr-black); }
.games-hero--photo .story-hero__media picture { position: absolute; inset: 0; display: block; }
.games-hero--photo .story-hero__media img { width: 100%; height: 100%; object-fit: cover; object-position: center right; display: block; }
.games-hero--photo .games-hero__text { position: relative; z-index: 1; }
/* As flex items the .container children shrink-to-fit and their auto side
   margins then centre them; force full width so content aligns to the left
   gutter (same edge as the nav logo). */
.games-hero--photo > .container { width: 100%; }
/* Text hugs the container's left edge (the darkest part of the image); the
   lede is the only width cap, so nothing sprawls under the subject on the right. */
.games-hero--photo .story-hero__title { font-size: clamp(2.6rem, 6vw, 5.6rem); }
.games-hero--photo .games-hero__lede { max-width: 46ch; font-size: 1.05rem; }
@media (max-width: 700px) {
  .games-hero--photo { min-height: clamp(420px, 70vh, 600px); }
  /* The phone frame is portrait against a landscape banner, so cover shows
     only ~36% of the image width and the choice of WHICH 36% is the whole
     picture. "right" (the desktop value) landed on the mother and daughter
     and cut the son out of the frame entirely — the reaching arm that makes
     the photograph. 70% holds son, father and the mother's face; the daughter
     cannot join them, because the three of them already span 880px of a 619px
     window. Past ~76% the son starts losing his face to the left edge.
     This figure is tied to the hero's HEIGHT: the window is 390px wide at
     whatever scale `cover` picks, so a taller hero sees less width and the
     same percentage frames differently. It was re-chosen when the lede was
     shortened (hero 613px -> 591px, window 597px -> 619px). Re-check it if
     the copy or min-height changes again.
     Desktop is unaffected: there the image fits on width and crops
     vertically, so the horizontal term does nothing. */
  .games-hero--photo .story-hero__media img { object-position: 70% center; }
  /* This used to veil the WHOLE frame (0.6 top, never below 0.52 up high) to
     keep the eyebrow and back link legible over the old hero's bright rope.
     Both of those elements were removed on 2026-08-14 and the rope belongs to
     an image that is gone, so the wash was darkening a top half with no type
     left in it — on an already-night photograph that read as crushed.
     Now weighted to the bottom, where the headline and lede actually sit.
     Measured on the new banner: nav 4.7:1, headline 8.3:1, lede 5.6:1 against
     the brightest 5% of their own bands, all clear of AA with the upper frame
     twice as bright as before. Re-measure if the hero image changes. */
  .games-hero--photo .story-hero__shade {
    background: linear-gradient(to bottom,
      rgba(12, 17, 16, 0.3) 0%,
      rgba(12, 17, 16, 0.18) 38%,
      rgba(12, 17, 16, 0.72) 78%,
      var(--norr-black) 100%);
  }
}


/* ---- print: cabin pages as an outreach PDF ------------------------------
   A cabin page printed to PDF is what we attach when asking a host for
   permission to feature them, so it has to look like the site rather than
   like a document. Backgrounds are forced on; the animated aurora canvas and
   the grain overlay are hidden because Chrome rasterises those full-page
   layers at print resolution (they alone turned a 4-page PDF into 30 MB).
   Generated by scripts/cabin-page-pdf.py, and equally by Cmd-P in a browser. */
@media print {
  *, *::before, *::after {
    -webkit-print-color-adjust: exact !important;
    print-color-adjust: exact !important;
  }
  @page { size: A4; margin: 10mm; }
  html, body { background: #0C1110 !important; }

  /* Full-page decorative layers: invisible on paper, enormous in the file. */
  .aurora, .aurora__canvas, .aurora__veil, .grain, canvas { display: none !important; }

  /* Site furniture has no place in a one-cabin document. */
  .nav, .footer, .cart-overlay, .cabin-back-wrap { display: none !important; }
  /* the outreach PDF keeps the hero and drops the carousel — a reader on paper
     cannot scroll it, and the old thumbnail strip was hidden here for the same reason */
  .cabin-gallery { display: none !important; }

  /* On screen the section breathes with viewport-relative padding; in print
     that became a near-empty first page. */
  .cabin { padding-block: 0 !important; }
  .cabin--aurora { overflow: visible !important; }

  /* Keep a photo and its caption on one sheet rather than sliced in half. */
  .cabin__inner { display: block !important; }
  .cabin__media, .cabin__body, figure, picture, img { break-inside: avoid !important; }
  img, picture { max-width: 100% !important; height: auto !important; }
  .cabin__media img { max-height: 150mm !important; object-fit: cover !important; }

  a[href]::after { content: none !important; }
}

/* "More cabins" preview grid: eleven cards plus a +N tile.
   Fifteen cabins in one grid read as a directory rather than a suggestion, so
   the tail is folded behind a counter. Hidden with display:none rather than
   visibility so the grid does not keep empty cells; the reveal tweens for the
   folded cards fire while hidden and land on their finished state, which is
   why opening shows them immediately instead of blank. */
.cabmore__extra { display: none; }
.cabmore.is-open .cabmore__extra { display: block; }
.cabmore.is-open .cabmore__more { display: none; }

.cabmore__more {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  /* align-self:start matters: a stretched grid item that also carries an
     aspect-ratio resolves its size against the row height, and the row then
     mis-sizes — the last row overflowed the grid box and collided with the
     "All cabins" link below it. Starting the tile keeps its height purely
     from the ratio. */
  align-self: start;
  aspect-ratio: 4 / 5;
  width: 100%;
  padding: 0;
  background: transparent;
  border: 1px solid rgba(238, 237, 232, 0.22);
  color: var(--c-muted);
  cursor: pointer;
  transition: border-color 0.25s ease, color 0.25s ease, background-color 0.25s ease;
}
.cabmore__more:hover,
.cabmore__more:focus-visible {
  border-color: rgba(238, 237, 232, 0.5);
  background: rgba(238, 237, 232, 0.04);
  color: var(--c-text);
}
.cabmore__plus {
  font-family: var(--font-display);
  font-size: clamp(1.8rem, 3.4vw, 2.6rem);
  line-height: 1;
  letter-spacing: -0.02em;
}
.cabmore__label {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  text-align: center;
  padding-inline: 0.75rem;
}

/* =========================================================
   Explore our range — four full-bleed doors: tees, hoodies,
   beanies, cabins. Markup: partials/home-range.njk. No JS —
   static 4-up row on desktop, snap-scroll strip under 800px.
   Hover: 1.6s cinematic zoom in, 0.5s settle out.
   ========================================================= */
.explore {
  padding-block: var(--section-y) 0;
}
.explore__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem;
  margin-bottom: clamp(2rem, 5vh, 3.25rem);
}
/* Full-bleed strip: three cards fill the viewport edge to edge, the fourth
   peeks in to invite the scroll. Snap-scrolling everywhere; explore.js adds
   drag-to-scroll for mouse users. */
.explore__grid {
  display: grid;
  grid-auto-flow: column;
  /* Track width carries BOTH constraints. If only the card capped its
     height (82vh) the track kept the full 1/3-viewport width and the
     leftover track space rendered as giant phantom gaps on short or
     very wide windows. */
  grid-auto-columns: min(
    calc((100vw - 2 * var(--xp-gap)) / 3.12),
    calc(82vh * 10 / 13)
  );
  --xp-gap: clamp(0.5rem, 0.9vw, 1rem);
  /* the row bleeds right, but the first card gets the same breathing
     room on the left as the cards keep between themselves */
  padding-inline-start: var(--xp-gap);
  scroll-padding-inline-start: var(--xp-gap);
  gap: var(--xp-gap);
  width: 100%;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
  cursor: grab;
  user-select: none;
}
.explore__grid::-webkit-scrollbar { display: none; }
.explore__grid.is-dragging { cursor: grabbing; scroll-snap-type: none; }
/* Mouse users drag freely — Chrome's proximity snap is greedy enough at
   these card widths to yank small nudges back home, which kills the
   feel. Fingers keep native snap; momentum there is the OS's job. */
@media (hover: hover) and (pointer: fine) {
  .explore__grid { scroll-snap-type: none; }
}
.explore-card {
  position: relative;
  display: block;
  aspect-ratio: 10 / 13;
  overflow: hidden;
  color: var(--snow);
  text-decoration: none;
  isolation: isolate;
  scroll-snap-align: start;
}
.explore__grid.is-dragging .explore-card { pointer-events: none; }
/* With three-across cards the second card's start sits beyond max scroll,
   so mandatory snap could never rest anywhere but 0 — the strip sprang
   back and the fourth card was unreachable. End-aligning the last card
   makes the far edge a legal snap position: two stable states, home and
   end. */
.explore-card:last-child { scroll-snap-align: end; }
.explore-card:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}
/* The media layer carries the zoom. Base transition is the 0.5s settle;
   hover overrides it with the long cinematic glide, so entering is slow
   and leaving is quick — the asymmetry is what reads as intentional. */
.explore-card__media {
  position: absolute;
  inset: 0;
  z-index: -1;
  transform: scale(1.001);
  transition: transform 0.5s cubic-bezier(0.33, 0, 0.2, 1);
  will-change: transform;
}
.explore-card:hover .explore-card__media,
.explore-card:focus-visible .explore-card__media {
  transform: scale(1.07);
  transition: transform 1.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.explore-card__media img,
.explore-card__media picture {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  -webkit-user-drag: none;
}
/* film grain so the duotones read as material, not flat fills */
.explore-card__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
  background-size: 160px 160px;
  opacity: 0.09;
  mix-blend-mode: overlay;
  pointer-events: none;
}
/* placeholder duotones until the photography lands — one light source each */
.explore-card__media--tees {
  background:
    radial-gradient(120% 80% at 70% 18%, rgba(226, 182, 111, 0.24), transparent 60%),
    linear-gradient(165deg, #55654f 0%, #22302a 55%, #0c1110 100%);
}
.explore-card__media--hoodies {
  background:
    radial-gradient(100% 70% at 28% 14%, rgba(169, 176, 172, 0.26), transparent 55%),
    linear-gradient(160deg, #2a3230 0%, #161b1a 60%, #0c1110 100%);
}
.explore-card__media--beanies {
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(250, 250, 247, 0.28), transparent 55%),
    linear-gradient(170deg, #8f9894 0%, #4d5a52 60%, #1a1d1c 100%);
}
.explore-card__media--cabins {
  background:
    radial-gradient(90% 60% at 55% 22%, rgba(226, 182, 111, 0.42), transparent 60%),
    linear-gradient(165deg, #4a3a26 0%, #2a2018 55%, #0c1110 100%);
}
.explore-card__media--games {
  /* firelight from below — the table the games get played on */
  background:
    radial-gradient(110% 55% at 50% 96%, rgba(226, 182, 111, 0.5), transparent 62%),
    linear-gradient(160deg, #23301f 0%, #1a2018 55%, #0c1110 100%);
}
/* scrim under the text; deepens slightly while the zoom runs */
.explore-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: linear-gradient(180deg, rgba(12, 17, 16, 0.12) 0%, rgba(12, 17, 16, 0.05) 40%, rgba(12, 17, 16, 0.55) 100%);
  opacity: 0.85;
  transition: opacity 0.6s ease;
  pointer-events: none;
}
.explore-card:hover::before,
.explore-card:focus-visible::before {
  opacity: 1;
}

/* Photo first: on a pointer device the card is only its photograph, and the
   title and mark fade up on hover. Gated behind `hover: hover` on purpose —
   a touch device never gets the hover state, so leaving the label hidden
   there would turn the strip into five unlabelled pictures. Hiding with
   opacity, not `display`, keeps the link's accessible name intact, so the
   cards are still properly labelled for screen readers while invisible.
   The scrim fades with the type: left at full strength over a photo with no
   text on it, it reads as a shadow smeared across the bottom edge. */
@media (hover: hover) and (pointer: fine) {
  .explore-card__content {
    opacity: 0;
    transition: opacity 0.45s ease, transform 0.5s cubic-bezier(0.33, 0, 0.2, 1);
  }
  .explore-card::before {
    opacity: 0;
  }
  .explore-card:hover .explore-card__content,
  .explore-card:focus-visible .explore-card__content {
    opacity: 1;
  }
  .explore-card:hover::before,
  .explore-card:focus-visible::before {
    opacity: 0.95;
  }
}
@media (prefers-reduced-motion: reduce) {
  .explore-card__content,
  .explore-card::before {
    transition: none;
  }
}
.explore-card__content {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-end;
  gap: 0.45rem;
  text-align: left;
  padding: clamp(1.2rem, 2vw, 1.9rem);
  transition: transform 0.5s cubic-bezier(0.33, 0, 0.2, 1);
}
.explore-card:hover .explore-card__content,
.explore-card:focus-visible .explore-card__content {
  transform: translateY(-5px);
  transition: transform 1.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.explore-card__count {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--snow) 78%, transparent);
  margin: 0;
}
/* photographs put anything behind the text — a wide soft shadow keeps the
   overlay legible without a heavier scrim dulling the image */
.explore-card__count,
.explore-card__title {
  text-shadow: 0 1px 22px rgba(12, 17, 16, 0.55), 0 1px 4px rgba(12, 17, 16, 0.35);
}
.explore-card__title {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 2.4vw, 2.4rem);
  font-weight: 600;
  letter-spacing: 0.01em;
  margin: 0;
}
/* The mark under the title is the brand lockup, not the reference's gold
   bar: split amber horizon, snow chevron rising between — the same mark
   that sits on the tees' chests. On hover the horizon stretches from the
   left, the dawn getting wider. */
/* The mark under the title is the real Norrwild lockup (Alex's file,
   2026-08-12) — snow chevron, tapered amber horizons. Uniform scale on
   hover; never stretch it. */
/* Above the title, not below. The card used to read count → title → mark;
   with the count gone the mark was left dangling under the headline and
   pressed against the bottom edge, reading as an underline rather than a
   mark. Moved up, it takes the eyebrow slot the count vacated and the title
   lands last and largest, which is the word you actually want read.
   `order: -1` rather than reordering the markup: the heading stays first in
   the DOM, so the link's accessible name is still the title. */
.explore-card__mark {
  order: -1;
  width: 68px;
  margin-bottom: 0.55rem;
  transform-origin: left center;
  transition: transform 0.5s cubic-bezier(0.33, 0, 0.2, 1);
}
.explore-card__mark img,
.explore-card__mark picture {
  display: block;
  width: 100%;
  height: auto;
  -webkit-user-drag: none;
}
.explore-card:hover .explore-card__mark,
.explore-card:focus-visible .explore-card__mark {
  transform: scale(1.08);
  transition: transform 1.6s cubic-bezier(0.16, 1, 0.3, 1);
}
@media (max-width: 800px) {
  .explore__grid {
    grid-auto-columns: 82vw;
    /* Phones snap HARD, one card per swipe (Alex, 2026-08-13). The base track
       is `proximity`, which only snaps when a swipe already lands near a card
       edge — a short flick just stopped half way and sat there. At 82vw there
       is one card per screen and a snap point every card, so `mandatory` has
       somewhere legal to land in every direction.

       Deliberately scoped to this breakpoint. Do NOT promote it to the base
       rule: above 800px the cards go three-across, the second card's start
       sits past max scroll, and mandatory would have nowhere to rest but 0 —
       the strip springs back and the far cards become unreachable. That bug
       is why the base says proximity. See the note by .explore-card:last-child. */
    scroll-snap-type: x mandatory;
  }
}
/* Reduced motion: the site's global html.reduced class governs, plus the
   media query as belt and braces — layout keeps, choreography goes. */
html.reduced .explore-card__media,
html.reduced .explore-card__content,
html.reduced .explore-card::before {
  transition: none !important;
}
@media (prefers-reduced-motion: reduce) {
  .explore-card__media,
  .explore-card__content,
  .explore-card::before {
    transition: none !important;
  }
  .explore-card:hover .explore-card__media { transform: none; }
}


/* =========================================================
   Prose pages — FAQ and legal. Words breathe: a reading
   measure inside the container, house type scale.
   ========================================================= */
.faqpage__inner,
.legalpage__inner {
  max-width: 68ch;
  padding-block: var(--section-y);
}
.faqpage__lede {
  color: var(--c-muted);
  max-width: 52ch;
  margin-top: 1rem;
}
.faqpage__head,
.legalpage__head {
  margin-bottom: clamp(2rem, 5vh, 3rem);
}
.faqpage__item {
  padding-block: 1.6rem;
  border-top: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
}
.faqpage__item h2 {
  font-family: var(--font-display);
  font-size: 1.25rem;
  margin-bottom: 0.7rem;
}
.faqpage__item p + p {
  margin-top: 0.75em;
}
.faqpage__item--sizing .measurement-guide {
  margin-top: 1.5rem;
}
@media (max-width: 560px) {
  .homefaq .measurement-guide,
  .faqpage .measurement-guide {
    grid-template-columns: 1fr;
    gap: 1.5rem;
    max-width: 17rem;
    margin-inline: auto;
  }
}
.legalpage__inner h2 {
  font-family: var(--font-display);
  font-size: 1.15rem;
  margin: 2rem 0 0.6rem;
}
.legalpage__inner p + p { margin-top: 0.7em; }
/* Model withdrawal form (Annex I(B) of Directive 2011/83/EU). Set apart from
   the prose because it is a form a buyer is meant to copy, not a clause they
   read past. A list rather than a <pre>: the statutory text is a set of lines,
   and a pre would overflow the reading column on a phone. */
.legalpage__inner .legal__formtitle {
  font-family: var(--font-display);
  font-size: 1rem;
  margin: 1.8rem 0 0.6rem;
}
.legal__form {
  margin: 0.9rem 0 0.6rem;
  padding: 1.2rem 1.4rem;
  list-style: none;
  border: 1px solid color-mix(in srgb, var(--ink) 18%, transparent);
  font-size: 0.94rem;
  line-height: 1.6;
}
.legal__form li + li {
  margin-top: 0.55em;
}
.legal__formnote {
  color: var(--c-muted);
  font-size: 0.85rem;
}
.legalpage__updated {
  color: var(--c-muted);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

/* =========================================================
   USP bar — quiet, mono, one line of true things.
   ========================================================= */
.uspbar {
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  display: flex;
  justify-content: center;
  gap: 0.7rem;
  padding: 0.55rem var(--gutter);
  font-family: var(--font-mono);
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--fog);
  background: var(--norr-black);
  border-bottom: 1px solid color-mix(in srgb, var(--fog) 18%, transparent);
}
@media (max-width: 640px) {
  .uspbar span:nth-child(n+4) { display: none; } /* two claims fit a phone */
}

/* =========================================================
   Sticky mobile CTA on product pages — glides back to the
   buybox; never rendered on desktop.
   ========================================================= */
.pdp-sticky {
  display: none;
}
@media (max-width: 800px) {
  .pdp-sticky {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 60;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.8rem;
    padding: 0.7rem var(--gutter) calc(0.7rem + env(safe-area-inset-bottom));
    background: color-mix(in srgb, var(--snow) 92%, transparent);
    backdrop-filter: blur(10px);
    border-top: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
    transform: translateY(105%);
    transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
  }
  .pdp-sticky.is-shown { transform: translateY(0); }
  .pdp-sticky[hidden] { display: flex; } /* visibility is driven by the class */
  .pdp-sticky__price {
    font-family: var(--font-display);
    font-weight: 600;
    color: var(--ink);
  }
  .pdp-sticky__btn { flex: none; }
}
