/* ============================================================================
   02-PRIMITIVES — composable layout primitives

   THE CORE IDEA: not one of these uses a media query.

   A breakpoint is a guess about content you haven't seen yet. Every breakpoint
   is a chance to guess wrong, and a 40%-longer German string is exactly the
   case that proves the guess wrong. These primitives adapt to the space they
   are given and the content they are handed, so there is no breakpoint to get
   wrong and no "it broke at 414px" retry.

   Nine primitives compose into all 49 blocks. ~4 KB total.
   ============================================================================ */

/* --- GLOBAL OVERFLOW GUARDS ------------------------------------------------
   Four rules that structurally prevent the overflow class of bug.           */

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

/* The #1 cause of horizontal scroll: flex/grid children default to
   min-width:auto and refuse to shrink below their content. This kills it. */
:where(.stack, .cluster, .switcher, .grid, .sidebar, .reel) > * { min-inline-size: 0; }

/* Long unbroken strings — German compounds, URLs, "Hengjanefossen" — wrap
   instead of forcing the page wider.
   `a` is in this list because the layout auditor caught "Hengjanefossen"
   pushing the footer 12px wide at 360px. Link text is exactly where long
   proper nouns live. */
:where(p, li, dd, dt, h1, h2, h3, h4, figcaption, blockquote, td, th, a, span) {
  overflow-wrap: break-word;
  hyphens: auto;
}

/* Media never exceeds its container, and never causes layout shift. */
:where(img, svg, video, canvas, iframe) {
  max-inline-size: 100%;
  block-size: auto;
}

/* --- 1. STACK — vertical rhythm -------------------------------------------
   The only thing that should ever create vertical space between siblings.
   Owl selector: margin goes BETWEEN children, never on the outside, so a
   stack never adds unexpected leading/trailing space.                       */
.stack {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
}
.stack > * { margin-block: 0; }
/* .stack.stack, not .stack — doubling the class raises specificity to (0,2,0).
   Blocks legitimately reset their own margins (`.inset-list { margin: 0 }`,
   `.timeline { margin: 0 }`, `.wchart { margin: 0 }`), and those rules live in
   a later file at equal specificity, so they were silently winning and the
   spacing a Stack promised never appeared. A layout primitive has to outrank
   a component's own reset or it cannot do its job. */
.stack.stack > * + * { margin-block-start: var(--stack-space, var(--space-m)); }

.stack[data-space="2xs"] { --stack-space: var(--space-2xs); }
.stack[data-space="xs"]  { --stack-space: var(--space-xs); }
.stack[data-space="s"]   { --stack-space: var(--space-s); }
.stack[data-space="l"]   { --stack-space: var(--space-l); }
.stack[data-space="xl"]  { --stack-space: var(--space-xl); }
.stack[data-space="2xl"] { --stack-space: var(--space-2xl); }

/* --- 2. CENTER — horizontal containment -----------------------------------
   Padding is inside the max-width, so content never touches a screen edge —
   the most common mobile layout complaint, solved once.                     */
.center {
  box-sizing: content-box;
  margin-inline: auto;
  max-inline-size: var(--center-max, var(--content-max));
  padding-inline: var(--gutter);
}
.center[data-measure] { --center-max: var(--measure); }
/* One page width. Sections previously mixed 60rem and 76rem containers, which
   made the gallery and reviews visibly wider than everything above them and the
   page read as misaligned. Reading measure is enforced per-element (.lede,
   .prose, p) rather than by narrowing the container. */
.center[data-wide]    { --center-max: 76rem; }

/* --- 3. BOX — a padded surface --------------------------------------------
   Sets both background and colour together, so a box is never light text on
   a light background regardless of where it is nested.                      */
.box {
  padding: var(--box-pad, var(--space-m));
  background: var(--box-bg, transparent);
  color: var(--box-fg, var(--label));
  border-radius: var(--box-radius, 0);
}

/* --- 4. CLUSTER — a group that wraps -------------------------------------
   For anything of unknown quantity and length: nav items, tags, meta rows,
   button pairs. It wraps. It cannot overflow. That is the entire job.      */
.cluster {
  display: flex;
  flex-wrap: wrap;
  gap: var(--cluster-gap, var(--space-s));
  align-items: var(--cluster-align, center);
  justify-content: var(--cluster-justify, flex-start);
}
.cluster[data-justify="between"] { --cluster-justify: space-between; }
.cluster[data-justify="center"]  { --cluster-justify: center; }
.cluster[data-align="start"]     { --cluster-align: flex-start; }

/* --- 5. SWITCHER — side-by-side until it shouldn't be ---------------------
   Two or more items sit in a row; below --threshold of AVAILABLE space they
   switch to stacked. Not viewport width — the space the component actually
   has. So it works identically in a sidebar, a modal, or full-bleed.
   No media query. Nothing to get wrong.                                    */
.switcher {
  display: flex;
  flex-wrap: wrap;
  gap: var(--switcher-gap, var(--space-l));
}
.switcher > * {
  flex-grow: 1;
  flex-basis: calc((var(--switcher-threshold, 30rem) - 100%) * 999);
}
/* Beyond this count, force stacking — 5 items in a row is never right. */
.switcher > :nth-last-child(n+5),
.switcher > :nth-last-child(n+5) ~ * { flex-basis: 100%; }

.switcher[data-threshold="s"] { --switcher-threshold: 20rem; }
.switcher[data-threshold="l"] { --switcher-threshold: 40rem; }
/* Weighted split, e.g. content + rail */
.switcher[data-split="2-1"] > :first-child { flex-grow: 2; }

/* --- 6. GRID — as many columns as fit -------------------------------------
   min() is load-bearing: without it, a 20rem minimum on a 360px screen
   overflows. With it, the column can never exceed the container.           */
.grid {
  display: grid;
  gap: var(--grid-gap, var(--space-l));
  grid-template-columns: repeat(
    auto-fit,
    minmax(min(var(--grid-min, 16rem), 100%), 1fr)
  );
}
.grid[data-min="s"] { --grid-min: 11rem; }
.grid[data-min="l"] { --grid-min: 21rem; }

/* --- 7. REEL — horizontal scroller with snap ------------------------------
   iOS App Store / Photos behaviour. Native momentum, no JS.
   --peek controls how much of the next card shows: the affordance that tells
   a user "this scrolls" without a visible scrollbar.                        */
.reel {
  display: flex;
  gap: var(--reel-gap, var(--space-m));
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--gutter);
  padding-inline: var(--gutter);
  padding-block-end: var(--space-xs);
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.reel::-webkit-scrollbar { display: none; }
.reel > * {
  flex: 0 0 var(--reel-item, calc(100% - var(--reel-peek, 12%)));
  scroll-snap-align: start;
  max-inline-size: 32rem;
}
.reel[data-item="s"] { --reel-item: min(72%, 18rem); }
.reel[data-item="m"] { --reel-item: min(86%, 26rem); }

/* --- 8. FRAME — fixed-ratio media ----------------------------------------
   Reserves space before the image loads. Zero cumulative layout shift,
   which is both a Core Web Vital and the "page jumped while I was reading"
   annoyance.                                                                */
.frame {
  position: relative;
  overflow: hidden;
  aspect-ratio: var(--frame-ratio, 4 / 3);
  background: var(--gray-6);
  border-radius: var(--frame-radius, 0);
}
.frame > :where(img, video, picture, svg) {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}
.frame[data-ratio="square"]   { --frame-ratio: 1 / 1; }
.frame[data-ratio="portrait"] { --frame-ratio: 3 / 4; }
.frame[data-ratio="tall"]     { --frame-ratio: 9 / 16; }
.frame[data-ratio="wide"]     { --frame-ratio: 3 / 2; }

/* --- 9. SIDEBAR — content + rail that collapses ---------------------------
   The rail keeps its width until the pair no longer fits, then both go full
   width. No breakpoint.                                                     */
.sidebar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sidebar-gap, var(--space-xl));
}
.sidebar > :first-child { flex-basis: var(--sidebar-width, 18rem); flex-grow: 1; }
.sidebar > :last-child {
  flex-basis: 0;
  flex-grow: 999;
  min-inline-size: var(--sidebar-content-min, 55%);
}

/* --- Utility: visually hidden but read by screen readers ------------------ */
.visually-hidden:not(:focus):not(:active) {
  clip-path: inset(50%);
  block-size: 1px; inline-size: 1px;
  overflow: hidden; position: absolute; white-space: nowrap;
}

/* --- Motion respect ------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Sidebar on the trailing edge, content first in the DOM. An article must lead
   with its content for crawlers and for reading order; the CTA rail is
   secondary even though it sits on the right visually. Below the wrap point it
   falls beneath the article, which is the correct mobile order too. */
.sidebar[data-side="end"] > :first-child {
  flex-basis: 0;
  flex-grow: 999;
  min-inline-size: var(--sidebar-content-min, 55%);
}
.sidebar[data-side="end"] > :last-child {
  flex-basis: var(--sidebar-width, 18rem);
  flex-grow: 1;
  min-inline-size: 0;
}
