@font-face {
  font-family: "Quilting Title";
  src: url("assets/fonts/NewFont-Regular.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Satoshi";
  src: url("assets/fonts/Satoshi-Regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Geist Mono";
  src: url("assets/fonts/GeistMono-VariableFont_wght.ttf") format("truetype-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Backstop for the <meta name="color-scheme"> tag in every page head:
     some Android browsers (Samsung Internet's "Dark mode for websites",
     Chrome's "force dark") will auto-recolor/invert a light site that
     doesn't explicitly declare its supported color scheme -- shifting the
     exact palette chosen here. Declaring light-only tells the browser to
     leave these colors alone regardless of the device's system/night
     mode. */
  color-scheme: light only;
  --paper: #fffdfa;
  --paper-top: #fffefb;
  --paper-batting: #f7f2e9;
  --paper-batting-warm: #efe1cf;
  --paper-backing: #ece8dd;
  --paper-backing-fiber: rgba(101, 91, 70, 0.16);
  --paper-2: #f2eadc;
  --ink: #171717;
  --line: rgba(23, 23, 23, 0.26);
  --main-blue: #344fa3;
  --main-lila: #806db0;
  --main-hellblau: #b9e4f3;
  --main-hellgelb: #fff1a8;
  --blue: var(--main-hellblau);
  --blue-dark: var(--main-blue);
  --lavender: var(--main-lila);
  --butter: var(--main-hellgelb);
  --display: "Quilting Title", "Arial Black", Impact, sans-serif;
  --body-font: "Satoshi", -apple-system, "Helvetica Neue", Arial, sans-serif;
  --mono-font: "Geist Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace;
  /* Type scale, rounded to a clean set of steps and expressed in rem so
     the whole site scales with a visitor's own browser/zoom font-size
     setting instead of staying pinned to fixed pixels (accessibility).
     Each still uses clamp() to stay fluid across viewport widths rather
     than jumping at fixed breakpoints, but the rem minimum/maximum are
     now deliberate, rounded values instead of ad-hoc per-section pixel
     guesses -- previously nearly a dozen sections each hand-tuned their
     own slightly different h2/body clamp, which read as an inconsistent
     scale when pages were compared side by side.
     --text-xs    ~11-12px  uppercase kickers/nav/labels
     --text-sm    ~12-14px  figure captions
     --text-base  ~16-18px  body copy (the site's reading size)
     --text-large ~18-22px  lead/statement paragraphs
     --text-h3    ~20-32px  sub-headings
     --text-h2c   ~28-54px  section headings paired with a photo/column
     --text-h2    ~28-64px  full-width section headings */
  --text-xs: clamp(0.6875rem, 0.66rem + 0.12vw, 0.75rem);
  --text-sm: clamp(0.75rem, 0.7rem + 0.2vw, 0.875rem);
  --text-base: clamp(1rem, 0.94rem + 0.3vw, 1.125rem);
  --text-large: clamp(1.125rem, 1.05rem + 0.35vw, 1.375rem);
  --text-h3: clamp(1.25rem, 1.1rem + 0.9vw, 2rem);
  --text-h2-compact: clamp(1.75rem, 1.5rem + 1.7vw, 3.375rem);
  --text-h2: clamp(1.75rem, 1.5rem + 2.4vw, 4rem);
  /* Shared pull-quote size -- consolidates three separately hand-tuned
     quote treatments (interview-strip, transcript-pull, warm-gallery
     quote) that previously ranged from 17-34px into one step. */
  --text-quote: clamp(1.125rem, 1rem + 0.8vw, 1.75rem);
}

* {
  box-sizing: border-box;
}

/* Kills WebKit's default tap-highlight rectangle on touch devices. Without
   this, tapping any link (like the .sandwich-hotspot areas) briefly paints
   a solid gray/blue bar over the whole tapped rectangle -- invisible in a
   desktop screenshot or browser hover check, but very visible on a phone,
   and easy to mistake for a rendering bug in the color transition itself
   when it's really just the browser's own tap feedback. */
a {
  -webkit-tap-highlight-color: transparent;
}

/* Plain native scrolling only -- no smooth-scroll behavior. Smooth-scroll
   animates the browser's own scroll position asynchronously from the
   user's actual input, which can read as a lag/correction in anything
   (like the fold-logo) that tracks scroll position frame by frame. */

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--body-font);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

.page-top {
  background:
    linear-gradient(rgba(255, 254, 251, 0.992), rgba(255, 254, 251, 0.992)),
    repeating-linear-gradient(90deg, rgba(23, 23, 23, 0.012) 0 1px, transparent 1px 42px),
    var(--paper-top);
}

.page-batting {
  background:
    radial-gradient(circle at 12% 20%, rgba(255, 241, 168, 0.1), transparent 32%),
    radial-gradient(circle at 86% 28%, rgba(185, 228, 243, 0.08), transparent 34%),
    linear-gradient(rgba(247, 242, 233, 0.965), rgba(247, 242, 233, 0.965)),
    var(--paper-batting);
}

.page-backing {
  background:
    radial-gradient(circle at 22px 34px, var(--paper-backing-fiber) 0 0.8px, transparent 1.4px),
    radial-gradient(circle at 92px 78px, rgba(101, 91, 70, 0.11) 0 0.9px, transparent 1.5px),
    repeating-linear-gradient(18deg, transparent 0 54px, rgba(101, 91, 70, 0.07) 54px 55px, transparent 55px 118px),
    linear-gradient(rgba(236, 232, 221, 0.95), rgba(236, 232, 221, 0.95)),
    var(--paper-backing);
  background-size: 130px 130px, 170px 170px, 240px 240px, auto, auto;
}

body * {
  font-family: var(--body-font) !important;
}

/* Captions, photo credits, and quoted lines borrow the book's mono/italic
   caption voice; everything else stays on the Satoshi body font. */
figcaption,
blockquote {
  font-family: var(--mono-font) !important;
  font-style: italic;
  font-weight: 400;
  /* balance (not pretty) since these are both always short, pull-quote
     length blocks -- same reasoning as .hero-deck above: pretty only
     smooths the last line, balance spreads the whole short block evenly
     so a comma or period mid-sentence doesn't strand one word alone on
     its own line. */
  text-wrap: balance;
}

/* Headings use Geist Mono (upright, not italic); body copy stays Satoshi.
   The big pixel-block hero titles (.hero-copy h1) keep the display font. */
h2,
h3 {
  font-family: var(--mono-font) !important;
}

.hero-copy h1 {
  font-family: var(--display) !important;
}

body.stitching::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  background-image: radial-gradient(circle at 50% 50%, rgba(255, 248, 239, 0.3), transparent 36%);
  animation: breath 2.8s ease-in-out infinite;
  z-index: 30;
}

@keyframes breath {
  0%, 100% { opacity: 0.12; transform: scale(0.96); }
  50% { opacity: 0.55; transform: scale(1.02); }
}

img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Better line-break selection sitewide, most useful on the many
   text-align:justify paragraphs scattered across the site -- it looks at
   the whole paragraph rather than one line at a time, so it avoids the
   worst rivers of extra-wide word-spacing that a narrow justified column
   can otherwise produce. Purely a progressive enhancement: unsupported
   browsers just keep ordinary line-breaking. */
p {
  text-wrap: pretty;
}

a {
  color: inherit;
}

button {
  font: inherit;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.site-nav {
  position: sticky;
  top: 0;
  /* Higher than .fold-logo's z-index (40) -- the nav must always render
     above the traveling/landed logo, in both scroll directions, not get
     covered by it. */
  z-index: 50;
  display: grid;
  grid-template-columns: minmax(240px, 1fr) auto minmax(240px, 1fr);
  align-items: center;
  height: 58px;
  min-height: 58px;
  padding: 0 22px;
  background: rgba(255, 254, 251, 0.84);
  border-bottom: 1px solid var(--line);
  backdrop-filter: blur(18px) saturate(1.2);
}

.page-batting .site-nav {
  background: rgba(247, 242, 233, 0.88);
}

.page-backing .site-nav {
  background: rgba(236, 232, 221, 0.9);
}

.brand,
.nav-links,
.issue,
.eyebrow,
.section-kicker,
.text-link,
.tool-note,
.cloth-meta {
  font-size: var(--text-xs);
  font-weight: 780;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

figcaption {
  font-size: var(--text-sm);
  font-weight: 400;
  letter-spacing: 0.01em;
  text-transform: none;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
}

.brand-mark {
  display: block;
  width: 16px;
  height: 16px;
}

.brand-logo {
  display: block;
  width: auto;
  height: 34px;
}

.nav-links {
  display: inline-flex;
  gap: 4px;
  border: 1px solid var(--line);
  padding: 4px;
  justify-self: center;
}

.nav-links a {
  padding: 8px 12px;
  text-decoration: none;
  border-bottom: 2px solid transparent;
  transition: color 0.2s ease, border-color 0.2s ease;
}

/* Subtle marking instead of a solid color block: the active/hovered link
   just picks up the page's accent color and a thin underline. */
.nav-links a:hover,
.nav-links a.active {
  background: transparent;
  color: var(--main-blue);
  border-bottom-color: var(--main-blue);
}

.page-top .nav-links a:hover,
.page-top .nav-links a.active {
  color: var(--main-lila);
  border-bottom-color: var(--main-lila);
}

.page-batting .nav-links a:hover,
.page-batting .nav-links a.active {
  color: var(--main-blue);
  border-bottom-color: var(--main-blue);
}

.page-backing .nav-links a:hover,
.page-backing .nav-links a.active {
  color: var(--main-lila);
  border-bottom-color: var(--main-lila);
}

/* Direct shortcut to the site's interactive highlight. Moved out of the
   Top/Batting/Backing tab row entirely (it was reading as a cramped
   fourth tab in there) -- this is a separate, fixed floating pill that
   sits on its own in the bottom-right corner of the viewport on every
   page, independent of scroll position, so it never competes with the
   nav bar for space and reads clearly as an extra shortcut rather than
   part of the page's own navigation. */
.playground-jump {
  position: fixed;
  right: clamp(16px, 3vw, 32px);
  bottom: clamp(16px, 3vw, 32px);
  z-index: 45;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 13px 18px;
  border-radius: 999px;
  background: var(--main-hellgelb);
  color: var(--ink);
  font-size: 11px;
  font-weight: 780;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  box-shadow: 0 6px 20px rgba(23, 23, 23, 0.22);
  transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.25s ease;
}

.playground-jump:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 26px rgba(23, 23, 23, 0.28);
}

/* Hidden once the visitor has scrolled into the CTA/tool area itself (see
   playground-jump.js's IntersectionObserver on #toolCta) -- the button is
   only useful for getting there, so it steps out of the way once it has
   done its job instead of floating on top of the tool it just pointed to. */
.playground-jump.is-near-target {
  opacity: 0;
  pointer-events: none;
}

/* Per-page accent instead of one fixed color everywhere -- ties the button
   to whichever layer of the quilt-sandwich metaphor that page represents
   (home/top/batting/backing), the same palette already used for each
   page's own hero and headings. Batting keeps the original hellgelb set
   above, so no override needed there. Blau and lila are dark enough to
   need paper (not ink) text for contrast. */
.page-home .playground-jump {
  background: var(--main-blue);
  color: var(--paper);
}

.page-top .playground-jump {
  background: var(--main-hellblau);
  color: var(--ink);
}

.page-backing .playground-jump {
  background: var(--main-lila);
  color: var(--paper);
}

.playground-jump-arrow {
  font-size: 14px;
}

.issue {
  justify-self: end;
  width: min(32vw, 380px);
  overflow: hidden;
  text-align: right;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.issue {
  color: var(--ink);
}

.layer-hero {
  min-height: calc(100svh - 58px);
  height: calc(100svh - 58px);
  display: grid;
  grid-template-columns: minmax(300px, 0.72fr) minmax(380px, 1.28fr);
  gap: clamp(16px, 3vw, 34px);
  align-items: stretch;
  padding: clamp(18px, 3vw, 34px);
}

/* Each layer page opens on a single bold color block -- eyebrow, title,
   deck text, and the isometric layer illustration together -- using the
   same top/batting/backing combo as the homepage illustration hover
   (.sandwich-visual-wash / .sandwich-visual-word). */
.top-hero {
  background: var(--main-lila);
}

.batting-hero {
  background: var(--main-blue);
}

.backing-hero {
  background: var(--main-hellblau);
}

/* Full viewport tall -- its own screen, with real distance to the next
   section, rather than a compact intro strip. Plain free scrolling, no
   scroll-snap -- an earlier snap-to-section version read as too abrupt. */
.home-hero {
  min-height: 100svh;
  display: grid;
  place-items: center;
  /* Safe-by-default gap: works at any width without relying on
     .home-logo-wrap's min-height cushion (see below). The tighter,
     vw-based gap that hugs the landed logo more closely only turns on
     at >=1000px, in a min-width query further down, where that cushion
     is guaranteed to actually be there. Below 1000px .home-logo-wrap's
     min-height collapses to 0 (see the max-width:1000px breakpoint), so
     the old vw-based gap plus the eyebrow's -16px margin (also
     desktop-only now) used to net out to only a couple of px, or even
     overlap outright -- reproduced live at 850px width, not just on
     phones. Rather than keep patching every width range that turned
     out to hit this, the default is now simply safe everywhere, and the
     tight look is the opt-in exception for genuinely wide screens. */
  gap: 36px;
  /* Horizontal padding matches the site's other full-bleed hero/section
     wrappers (.section-pad, .workshop-trace, .tool-cta all use
     max(24px, 6vw)) -- this used to run its own max(20px, 7vw), which
     read as a slightly different left/right margin than neighboring
     sections when scrolling down the page. */
  padding: clamp(26px, 6vw, 88px) max(24px, 6vw);
  background:
    linear-gradient(90deg, rgba(255, 254, 251, 0.98), rgba(255, 254, 251, 0.72)),
    var(--paper);
}

/* width:100% keeps this a full-width band across the hero cell (rather
   than shrinking to the logo's own width), so the logo it contains
   centers reliably regardless of how wide the logo itself renders. */
.home-logo-wrap {
  display: grid;
  place-items: center;
  width: 100%;
  min-height: min(48svh, 460px);
  /* Nudges the logo's landed resting slot a little further down the hero
     than dead-center, so it doesn't sit quite so high on tall screens. */
  margin-top: clamp(16px, 4vw, 40px);
}

.home-copy {
  max-width: 60ch;
  text-align: center;
}

.home-copy .eyebrow {
  color: var(--ink);
  /* Safe default -- see the .home-hero gap comment above. The negative
     margin that pulls this up snug against the logo is now scoped to
     the >=1000px query further down, where .home-logo-wrap's min-height
     cushion actually exists to pull it into. */
  margin-top: 12px;
  margin-bottom: 10px;
}

/* Opt-in tight desktop look, restored only where .home-logo-wrap's
   min-height: min(48svh, 460px) cushion is guaranteed to be in effect
   (that cushion collapses to 0 below 1000px -- see the max-width:1000px
   breakpoint). At these widths the landed logo sits centered in a tall
   box with real empty space below it, so pulling the eyebrow up into
   that space with a small vw-scaled gap reads as intentional, tight
   spacing instead of an overlap. */
@media (min-width: 1000px) {
  .home-hero {
    gap: clamp(10px, 1.8vw, 24px);
  }

  .home-copy .eyebrow {
    margin-top: -16px;
  }
}

.home-copy h1 {
  text-align: center;
}

/* Blocksatz for the longer running paragraphs -- the eyebrow label and
   hidden h1 above stay centered (they're single short lines, not real
   body copy), but once there's an actual paragraph of text, justify
   reads calmer than every line being centered independently. */
.home-copy .hero-intro,
.home-copy .hero-deck {
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

.home-copy .hero-intro {
  max-width: 62ch;
  margin: 0 auto 18px;
  color: var(--ink);
  font-family: var(--body-font);
  font-size: var(--text-large);
  line-height: 1.35;
  /* Same fix as .hero-deck above -- balance instead of the inherited
     pretty, so this short intro doesn't strand a word or two alone on a
     line mid-paragraph. */
  text-wrap: balance;
}

.home-copy .hero-deck {
  color: var(--ink);
  margin-left: auto;
  margin-right: auto;
}

/* --- Fold intro: sticky scroll-scrubbed quilt-unfold sequence --- */
.fold-intro {
  position: relative;
  /* More scroll travel for the same 42 frames -- each frame swap now
     needs more scroll distance to trigger, so the sequence advances less
     abruptly per scroll gesture/tick. Was 520vh. */
  height: 680vh;
  background: var(--paper);
}

.fold-intro-sticky {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
  display: grid;
  place-items: center;
  background: var(--paper);
}

.fold-intro-stage {
  position: relative;
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
}

.fold-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* cover (not contain) so the frame always fills the whole sticky stage
     edge-to-edge -- these are wide 16:9 shots against a plain backdrop, so
     the modest crop this introduces just trims a little headroom rather
     than cutting into the quilt itself. */
  object-fit: cover;
  filter: none;
  box-shadow: none;
}

/* The scrub sequence renders as two stacked frames: a base image and this
   next-frame layer crossfading in on top as scroll moves between two
   neighboring shots. fold-intro.js drives its opacity directly, in sync
   with the scroll fraction (no CSS transition) so each frame blends into
   the next instead of popping -- a soft dissolve rather than a hard cut. */
.fold-frame-next {
  opacity: 0;
}

/* A flat dark layer over the unfolded quilt while the logo is visible on
   top of it -- the same flat-color treatment as .blanket-cover-media::after
   further down the page. A busy, colorful photo makes a plain-ink logo hard
   to read, so this dims the photo behind it while the logo itself (below)
   switches to white for the same reason. Fades back out once the sequence
   releases toward the plain hero background. */
.fold-intro-stage::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(23, 23, 23, 0.32);
  opacity: 0;
  transition: opacity 1.2s ease;
  pointer-events: none;
  z-index: 5;
}

.fold-intro.is-logo-stage .fold-intro-stage::after {
  opacity: 1;
}

/* The traveling logo lives outside the fold stage (direct child of <main>)
   so it can be position:fixed, independent of the sticky stage's own
   overflow. It fades in centered once the quilt has unfolded, then
   fold-intro.js tracks its fixed position frame-by-frame to the live
   position of the (empty) .home-logo-wrap slot as that section scrolls
   into view -- a scroll-coupled "landing", not a timed animation. Once
   fully landed it is moved into .home-logo-wrap and switched to .is-landed
   (position:static), becoming ordinary document content that just scrolls
   with the page.

   It is now a shell around TWO stacked copies of the same artwork rather
   than one image with an animated filter: a white (inverted) copy that
   sits in normal flow and shows by default, and a black (natural ink)
   copy layered exactly on top of it. Animating the invert() amount on a
   single image instead (the first attempt at this) interpolates through a
   muddy, indeterminate gray mid-way through -- it never reads as "fading
   to black," just briefly murky. A timed opacity crossfade between the two
   flat layers (the second attempt) avoided the muddy color but still read
   as either an instant flip or something that finished off-screen,
   depending on scroll speed, since it ran on its own clock instead of the
   scroll. Now the black copy's visibility is a clip-path driven directly
   by scroll progress (see .fold-logo-img--black below) -- the ink only
   fills in where the logo has actually crossed onto the white background,
   the same edge that triggers everything else about the landing. */
.fold-logo {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(40vw, 420px);
  display: block;
  opacity: 0;
  pointer-events: none;
  z-index: 40;
  transition: opacity 0.5s ease;
  /* fold-intro.js repositions this element every scroll frame during the
     release phase via `transform: translate(...)` (a GPU-composited
     property, not left/top) specifically so the browser doesn't have to
     re-run layout on every frame -- will-change hints that up front so
     the browser promotes it to its own compositor layer immediately,
     rather than only after the first transform change already happened. */
  will-change: transform;
}

.fold-logo.is-visible {
  opacity: 1;
}

.fold-logo.is-landed {
  /* relative, not static: a static box stops being a positioning context,
     which would let .fold-logo-img--black's absolute inset:0 escape to the
     nearest positioned ancestor instead of staying stacked exactly over
     the white copy -- breaking the crossfade right when it needs to run.
     Relative behaves like static for the wrapper's own place in the flow
     (no offsets applied) while still anchoring the black layer correctly. */
  position: relative;
  top: auto;
  left: auto;
  transform: none;
  margin: 0 auto;
  display: block;
}

.fold-logo-img {
  display: block;
  width: 100%;
  height: auto;
  object-fit: contain;
}

/* The default, visible-while-not-landed layer: inverted to near-white so
   it reads against the dark flat layer over the quilt photo above. */
.fold-logo-img--white {
  filter: invert(1) brightness(1.2);
}

/* Layered exactly over the white copy, in its natural black ink. Instead
   of a timed opacity fade (which either finished before anyone was looking
   or read as one all-at-once flip, depending on scroll speed), this is
   revealed by a clip-path that fold-intro.js drives frame-by-frame from
   the same scroll progress already used for the landing position -- so
   the black ink only appears where the logo has actually crossed onto the
   white hero background below, edge first, rather than the whole mark
   swapping color at once. No CSS transition here on purpose: this needs
   to track the scroll position 1:1, the same way the landing motion does,
   not ease toward it on its own timer. Base state is fully clipped (all
   white, no black yet); fold-intro.js overwrites this inline every frame. */
.fold-logo-img--black {
  position: absolute;
  inset: 0;
  clip-path: inset(100% 0 0 0);
}

/* position:fixed (not absolute) so this is always pinned to the real
   viewport bottom, not to .fold-intro-sticky's own box. At page load the
   sticky element still sits in its normal (pre-stuck) flow position --
   pushed down by the nav bar above it -- so its bottom edge, and this
   hint along with it, briefly sat just below the fold and only scrolled
   into view after a few px of scrolling. Fixed positioning shows it
   immediately, right from the first paint. */
.fold-intro-hint {
  position: fixed;
  left: 50%;
  bottom: clamp(20px, 4vw, 40px);
  transform: translateX(-50%);
  z-index: 6;
  margin: 0;
  font-size: 11px;
  font-weight: 780;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--main-blue);
  opacity: 1;
  transition: opacity 0.3s ease;
}

.fold-intro-hint.is-hidden {
  opacity: 0;
}

.home-copy h1 {
  max-width: 8ch;
  margin: 10px 0 clamp(34px, 3vw, 46px);
  font-family: var(--body-font);
  font-size: clamp(54px, 9vw, 128px);
  font-weight: 500;
  line-height: 0.9;
  letter-spacing: 0;
}

/* Full viewport height so the philosophy statement gets a whole screen
   to itself (like the hero moments elsewhere) instead of a compact block
   -- the kicker + manifesto grid center as one unit inside that height,
   with .section-pad's own padding still keeping edge breathing room. */
.home-concept {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  background: var(--paper);
  border-top: 1px solid var(--line);
}

.blanket-scroll {
  display: block;
  /* Horizontal padding matches the site's other content sections
     (.timeline-section, .warm-gallery, .aunt-archive all use
     max(20px, 5vw)) -- this used to run its own max(18px, 4vw), reading
     as a narrower left/right margin than the sections around it. */
  padding: clamp(34px, 5vw, 72px) max(20px, 5vw);
  background:
    linear-gradient(rgba(255, 253, 250, 0.97), rgba(255, 253, 250, 0.97)),
    repeating-linear-gradient(90deg, rgba(23, 23, 23, 0.02) 0 1px, transparent 1px 32px),
    var(--paper-top);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

/* Full-bleed cover moment before the blanket carousel: the photo fills the
   entire viewport width (no section padding) with the project's recurring
   tagline set in the big pixel display font, floating over it. Both the
   image and the line hold back slightly until the section scrolls into
   view, then settle into place -- a quiet reveal rather than an instant
   pop, in the same spirit as the fold-intro's soft crossfade.

   The photo and the type are split into two layers on purpose:
   .blanket-cover-media is the only element with overflow:hidden, and its
   aspect-ratio matches the photo exactly so the image is never cropped.
   .blanket-cover-line sits outside that clipped box (a sibling, absolutely
   positioned against the section itself), so at this type scale it can
   never get its top edge sheared off by the photo's own frame -- only the
   image is clipped, never the text. */
.blanket-cover {
  position: relative;
  width: 100%;
  background: var(--ink);
}

/* Height is capped to one viewport tall, so the whole moment -- photo and
   both lines of type -- is always visible together without scrolling.
   It still prefers the photo's true aspect-ratio height (no crop) and
   only falls back to the viewport cap -- letting object-fit:cover crop
   slightly -- on wide-but-short windows where the full-ratio height
   would run taller than one screen. */
.blanket-cover-media {
  position: relative;
  width: 100%;
  height: min(100svh, calc(100vw * 1734 / 2600));
  overflow: hidden;
}

.blanket-cover-media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  opacity: 0;
  transform: scale(1.08);
  transition: opacity 1.8s ease, transform 2.4s cubic-bezier(0.19, 1, 0.22, 1);
}

.blanket-cover.is-visible .blanket-cover-media img {
  opacity: 1;
  transform: scale(1);
}

/* A flat, uniform dark layer over the whole photo (not a gradient) so the
   white type reads clearly wherever it lands, at a light enough opacity
   that the quilt stack still shows through. */
.blanket-cover-media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(23, 23, 23, 0.32);
  pointer-events: none;
}

/* Book-scale type: each line is its own independently placed block (not a
   tidy centered web headline) -- bold enough to press right up against
   the edges of the frame, the way the printed spread sets it. The two
   lines are pinned to opposite corners rather than stacked as one block.
   Needs its own !important: the global
   `body * { font-family: var(--body-font) !important }` reset matches
   this element directly, so without this it would win and silently swap
   back to the body font. */
.blanket-cover-line {
  position: absolute;
  z-index: 2;
  margin: 0;
  max-width: min(90vw, 820px);
  font-family: var(--display) !important;
  font-weight: 400;
  text-transform: uppercase;
  /* leading set to exactly half the type size (line-height: 0.5 means
     0.5 x font-size, so 100pt type gets 50pt of line-height). At leading
     this tight, the font's actual ink -- especially its little corner
     dots -- sits taller and wider than the line box itself, so this
     padding (in em, scaling with the font-size) is a fixed buffer around
     the text on every side, independent of where the block is anchored. */
  line-height: 0.5;
  letter-spacing: 0;
  color: var(--paper);
  font-size: clamp(56px, 13vw, 240px);
  padding: 0.22em 0.1em;
  /* a single word at this size is often wider than the frame -- let it
     break mid-word onto another line instead of overflowing past the
     edge and getting sheared off. */
  overflow-wrap: anywhere;
  word-break: break-word;
  hyphens: none;
}

.blanket-cover-line.line-top {
  /* hugging the true corner now -- the em-based padding above (not this
     inset) is what actually keeps the font's little corner dots from
     being sheared off, so this can sit close to the edge. */
  top: clamp(4px, 1vw, 16px);
  left: clamp(4px, 1vw, 16px);
  text-align: left;
}

.blanket-cover-line.line-bottom {
  bottom: clamp(4px, 1vw, 16px);
  right: clamp(4px, 1vw, 16px);
  text-align: right;
}

/* A stitch-style reveal instead of a plain fade: each line is masked
   behind a clip-path and unstitches open left-to-right, like a seam
   being drawn back, the bottom line following a beat after the top one.
   Slower and gentler than the first pass -- the whole homepage is meant
   to read as calm, unhurried, not snappy. */
.blanket-cover-line {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 1.8s cubic-bezier(0.22, 1, 0.36, 1);
}

.blanket-cover.is-visible .blanket-cover-line {
  clip-path: inset(0 0 0 0);
}

.blanket-cover.is-visible .blanket-cover-line.line-bottom {
  transition-delay: 0.5s;
}

/* Kicker directly above the paragraph, single reading column -- matches
   the kicker/heading/paragraph stack used everywhere else on the site
   (.workshop-trace-head, .tool-cta, etc.). The old two-column layout
   (kicker pinned to a narrow left column, paragraph bottom-aligned in a
   much wider right column) read as two unrelated fragments with a wide
   gap between them instead of one held thought. */
.blanket-scroll-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  max-width: 56ch;
  /* Small extra top margin (on top of .blanket-scroll's own section
     padding) so the block sits a little lower in that opening space
     instead of starting right at the very top edge. */
  margin: clamp(16px, 3vw, 48px) auto clamp(64px, 11vw, 160px);
  text-align: center;
}

.blanket-scroll-head p:last-child {
  color: var(--ink);
  margin: 0;
  font-size: var(--text-base);
  line-height: 1.35;
}

.blanket-track {
  display: grid;
  /* Back to two columns per request. The odd count (9 quilts) would
     normally leave the last one alone in a half-empty final row, so it
     gets special-cased below to sit centered and full-width instead. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* Tighter than before -- the wide gutter here plus each photo's own
     light studio backdrop was reading as one big empty middle. */
  gap: clamp(14px, 2.4vw, 36px) clamp(10px, 2vw, 32px);
  overflow: visible;
  padding: 0;
}

.blanket-track figure {
  min-height: 0;
  margin: 0;
  display: block;
  background: transparent;
  /* Override the base figure{overflow:hidden} (meant for the overlaid,
     absolutely-positioned captions used elsewhere on the site). Here the
     figcaption flows in normal document flow below the image instead of
     overlapping it, so clipping the figure box also clips the caption --
     this was cutting off the right-aligned even-column captions right at
     their own edge. */
  overflow: visible;
}

.blanket-track img {
  width: 100%;
  height: auto;
  object-fit: contain;
}

.blanket-track figcaption {
  position: static;
  margin-top: clamp(20px, 3vw, 34px);
  padding: 0;
  background: transparent;
  border: 0;
  color: var(--ink);
}

/* Diagonal zigzag stagger -- dialed back down from an earlier, much
   taller version (clamp(80px, 16vw, 260px)) that was leaving huge empty
   gaps between rows on wide screens. */
.blanket-track figure:nth-child(even) {
  margin-top: clamp(40px, 8vw, 140px);
}

/* Alternating caption alignment: odd column reads left (under the image's
   left edge), even column reads right (under the image's right edge),
   matching the zigzag stagger of the images themselves. Both figcaptions
   are the same width as their figure (block-level, no cross-column
   overflow), so text-align: right here stays flush inside the even
   figure's own right edge rather than overhanging into the gutter. */
.blanket-track figure:nth-child(even) figcaption {
  text-align: right;
}

/* The 9th (last, odd-count) quilt sits in its normal column-1 spot, full
   size like every other item -- the source photo itself was recropped
   tighter in height (less empty studio backdrop above/below the quilt)
   so it naturally reads shorter and sits closer to the row above it,
   instead of being shrunk down via CSS. A small negative nudge pulls it
   up a bit further into its own gutter, per request -- the mirror image
   of the even-column stagger, just pulling up instead of pushing down. */
.blanket-track figure:nth-child(9) {
  margin-top: -40px;
}

.home-sandwich {
  display: grid;
  /* Visual panel now takes most of the section's width (was roughly
     43/57 intro/visual, now closer to 26/74) so the illustration itself
     gets much more room, per request. */
  grid-template-columns: minmax(260px, 0.5fr) minmax(460px, 1.55fr);
  gap: 12px;
  padding: 12px;
  /* Blue at rest -- both columns (and the thin gap between them) switch
     together per active layer below. Timing must match .sandwich-intro
     and .sandwich-visual-panel exactly (0.35s ease, not 0.4s): this 12px
     gap shows the .home-sandwich background directly, so any mismatch in
     duration makes the gap lag behind the two panels and appear as a
     visible bar of a different in-between color while hovering. */
  background: var(--main-blue);
  color: var(--main-hellgelb);
  transition: background-color 0.35s ease;
}

/* Kicker used to sit pinned to the top with the heading+copy stack
   filling the rest of the column below it -- same top/bottom split
   retired everywhere else on the site. Now the whole group (kicker,
   heading, description) centers together as one block via
   justify-content: center, matching that site-wide convention. */
.sandwich-intro {
  min-height: 640px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 20px;
  padding: clamp(24px, 4vw, 54px);
  background: var(--main-blue);
  transition: background-color 0.35s ease, color 0.35s ease;
}

.sandwich-intro .section-kicker {
  /* Spacing to the stack below now comes from .sandwich-intro's own flex
     gap, not this margin -- kept as margin from the old pinned-top
     layout, which would otherwise double up with the new gap. */
  margin-bottom: 0;
  color: var(--butter);
  transition: color 0.35s ease;
}

/* Used to stack all four states in one grid-area: 1/1 cell sized to the
   TALLEST of the four (the longer default intro text), then center each
   item within that shared tall cell -- but centering a short item (e.g.
   "Quilt Top") inside an oversized cell still leaves the extra space
   split evenly above/below the item itself, and since the .section-kicker
   sits right above the stack (not inside it), that upper half of dead
   space reads as "kicker hugging the h2 with a big gap at the bottom"
   rather than true balance against the whole purple block -- confirmed
   live at 1512px, not just on a shorter viewport. Fixed properly now: only
   the ACTIVE item stays in normal flow (position: static), so the stack's
   own height always matches exactly whichever state is showing; every
   other item is taken out of flow via position: absolute so it can't
   inflate the cell. With no leftover dead space, .sandwich-intro's own
   justify-content: center (kicker + stack, .sandwich-intro-copy comment
   above) centers the whole compact group correctly for every state. */
.sandwich-intro-stack {
  position: relative;
}

.sandwich-intro-item {
  position: absolute;
  inset: 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease;
}

.sandwich-intro-item.is-default {
  position: static;
  opacity: 1;
  visibility: visible;
}

/* Once any layer is active, the default item drops out of flow too (it
   would otherwise still be the one sizing the stack, defeating the point
   above) and whichever .sandwich-intro-item matches the active layer
   (rule further down, next to the other #homeSandwich[data-active-layer]
   swaps) becomes the in-flow one instead. */
#homeSandwich[data-active-layer] .sandwich-intro-item.is-default {
  position: absolute;
}

.sandwich-intro-item h2 {
  font-size: var(--text-h2-compact);
}

.sandwich-intro-copy {
  max-width: 34ch;
  margin: 16px 0 0;
  font-family: var(--body-font);
}

/* --- Sandwich visual: one illustration, three hover zones. JS toggles
   [data-active-layer] on #homeSandwich when a hotspot is hovered/focused;
   everything below reacts to that single attribute -- the highlighted
   illustration, the panel's own background, the page word, and (in the
   .sandwich-intro column) the heading and short description swap too.

   The panel's background transitions directly (background-color, same
   property and timing as .sandwich-intro right below) rather than through
   a separate absolutely-positioned "wash" layer fading in via opacity.
   Those were two different ways of animating color at once: a direct
   color transition interpolates straight from lila to blue, but an
   opacity fade of a solid blue layer over a lila base blends the two via
   alpha compositing, which passes through different in-between colors.
   Same duration, same easing, different math -- the two panels visibly
   disagreed mid-transition. One mechanism for both removes that. */
.sandwich-visual-panel {
  position: relative;
  overflow: hidden;
  min-height: 560px;
  background: var(--main-blue);
  transition: background-color 0.35s ease;
}

/* Hotspot boundaries follow the actual isometric drawing. Corrected after
   the left/right sides of batting and backing were confirmed (via a
   pixel-by-pixel column scan in PIL, at x=100 and x=950 of the source
   PNG) to not be switching: the earlier version assumed the top face's
   own left/right corners sit at the same point as the diamond's outer
   widest point (2.27%/97.84% width, ~61% down) -- verified correct FOR
   THE OUTER SILHOUETTE, but wrong for the top face itself. The top
   face's own left/right corners are much higher up, around 43% down,
   not 61% -- confirmed by scanning straight down the image at the left
   edge (x=100) and finding solid batting-tan starting around y=595-622
   of 1396, vs. the outer edge only reaching there around y=857. The two
   "left vertices" are different points on the same near-vertical edge:
   top of that edge is the top/batting seam, bottom of it is the outer
   silhouette corner. Below that, batting is a hexagon (its own
   left/right corners high up, dipping to a point behind the top face's
   near corner, opening back to the full outer width, then narrowing to
   another point at the batting/backing seam), and backing closes out
   the outer silhouette down to the front tip.

   Box below uses aspect-ratio (matching the source PNG's own 1896x1396
   ratio) instead of a plain inset, on purpose: a plain inset box whose
   shape doesn't match the image's own ratio still renders correctly on
   screen (object-fit: contain just letterboxes it), but that
   letterboxing means "percent of the box" no longer equals "percent of
   the actual pixels", which quietly breaks these polygons. Locking the
   box to the image's real ratio keeps 0-100% mapped 1:1 to the image
   content no matter how far left/how narrow the box gets shifted later.
   This box, and .sandwich-visual-img's, must always carry the exact
   same left/top/width/aspect-ratio. */
/* top: 50% + translateY(-50%) instead of a fixed top: 6% -- the frame's
   own size is set by width: 54% of the panel, so on a narrower viewport
   (panel column closer to its 460px minimum) the illustration shrinks a
   lot while the panel's own height stays pinned near .sandwich-intro's
   640px. Anchored 6% from the top, that shrinkage read as "small and
   stuck at the top" with a growing gap below on narrower/MacBook widths.
   Centering the group instead keeps it balanced in the panel regardless
   of how big or small it renders. Percentages inside the clip-path
   polygons below are relative to the hotspot's own box, not the panel,
   so this repositioning doesn't touch them. */
.sandwich-hotspot {
  position: absolute;
  left: 9%;
  top: 50%;
  width: 54%;
  aspect-ratio: 1896 / 1396;
  transform: translateY(-50%);
  z-index: 5;
  display: block;
}

.sandwich-hotspot--top {
  clip-path: polygon(50% 9.24%, 97.84% 42.98%, 50.11% 70.06%, 2.27% 42.98%);
}

/* Second bug, found the same way as the top/batting one above: the
   batting/backing seam on the LEFT and RIGHT sides does not sit at the
   outer silhouette's widest point either (2.27%/97.84% width, ~61%
   down) -- that point is the OUTER edge's own corner (where backing's
   silhouette is widest), not where batting's tan actually ends. Column
   scans down the image (classifying pixels as tan-ish when R-B is
   large, purple-ish when B>R) show the real tan/purple seam running
   from about (2.27%, 50.7%) down to the front seam (50.11%, 84.10%)
   and back up to about (97.84%, 50.5%) -- roughly 10-11 points higher
   up than the outer corner, not coincident with it. Previously batting
   reached all the way down to the outer corner on the sides, which
   silently ate the purple/backing area there -- backing's own polygon
   never had a chance to cover it, so hovering the purple band near the
   left/right edges fell through to nothing. */
.sandwich-hotspot--batting {
  clip-path: polygon(
    2.27% 42.98%,
    2.27% 50.72%,
    50.11% 84.10%,
    97.84% 50.50%,
    97.84% 42.98%,
    50.11% 70.06%
  );
}

.sandwich-hotspot--backing {
  clip-path: polygon(
    2.27% 61.39%,
    50.63% 97.06%,
    97.84% 61.03%,
    97.84% 50.50%,
    50.11% 84.10%,
    2.27% 50.72%
  );
}

/* Wrapper div carries the position + aspect-ratio; the <img>s just fill
   it at 100%/100%. Tested directly: a plain absolutely-positioned <img>
   given `width: 68%; aspect-ratio: 1896/1396;` (no wrapper) resolves its
   *height* to the containing block's height instead of the ratio, in
   this browser, specifically for replaced elements sized this way --
   an ordinary <div> with the identical width/aspect-ratio computes the
   correct height. Moving aspect-ratio onto a plain div and letting the
   images fill it sidesteps that replaced-element quirk entirely. */
.sandwich-visual-frame {
  position: absolute;
  left: 9%;
  top: 50%;
  width: 54%;
  aspect-ratio: 1896 / 1396;
  transform: translateY(-50%);
  pointer-events: none;
}

.sandwich-visual-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
}

.sandwich-visual-overlay {
  opacity: 0;
  transition: opacity 0.3s ease;
}

/* Three lines stacked, right-aligned: QUILTING and TIME stay fixed, only
   the middle line (.sandwich-visual-word-fill) swaps per layer -- reads
   like a small sampler label ("QUILTING / THROUGH / TIME") where only the
   fill-word changes as you move between hotspots. */
.sandwich-visual-word {
  position: absolute;
  top: 8%;
  right: 3%;
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  text-align: right;
  text-transform: uppercase;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
  z-index: 4;
}

/* Needs !important on each line, not just the parent: the global
   `body * { font-family: var(--body-font) !important }` reset matches
   these spans directly (inheritance from the parent doesn't help against
   a rule with equal specificity + !important on the element itself).
   QUILTING and TIME match the fill word's size exactly now -- all three
   lines read as one equal-weight phrase rather than two small labels
   flanking a bigger headline -- and the whole block is sized up overall
   (bumped a fourth time, larger again, now that the illustration has
   shifted further left and freed up even more space for it). Leading
   follows the house rule of exactly half the type size (see the comment
   near line 623): line-height 0.5 means 0.5 x font-size. */
.sandwich-visual-word-line,
.sandwich-visual-word-fill {
  font-family: var(--display) !important;
  font-size: clamp(60px, 7.5vw, 132px);
  line-height: 0.5;
}

.sandwich-visual-word[data-layer="top"] {
  color: var(--main-hellblau);
}

/* Yellow, not blue -- batting's background is blue again (matching the
   resting default), so the word needs the pale yellow to stay readable. */
.sandwich-visual-word[data-layer="batting"] {
  color: var(--main-hellgelb);
}

.sandwich-visual-word[data-layer="backing"] {
  color: var(--main-lila);
}

#homeSandwich[data-active-layer="top"] .sandwich-visual-overlay[data-layer="top"],
#homeSandwich[data-active-layer="top"] .sandwich-visual-word[data-layer="top"],
#homeSandwich[data-active-layer="batting"] .sandwich-visual-overlay[data-layer="batting"],
#homeSandwich[data-active-layer="batting"] .sandwich-visual-word[data-layer="batting"],
#homeSandwich[data-active-layer="backing"] .sandwich-visual-overlay[data-layer="backing"],
#homeSandwich[data-active-layer="backing"] .sandwich-visual-word[data-layer="backing"] {
  opacity: 1;
}

/* The left (text) column and the illustration panel both switch to the
   same color together (as does the thin gap-seam of .home-sandwich
   itself), all three driven by the same background-color transition, so
   the whole panel visibly commits to whichever layer is active with no
   mismatch between sides. Rest state is blue; Top is lila (swapped back
   from an earlier pass that had these reversed). Text flips to hellblau on
   the lila ground -- the same lila+hellblau pair used in reverse for
   Backing below (hellblau ground, lila text). */
#homeSandwich[data-active-layer="top"],
#homeSandwich[data-active-layer="top"] .sandwich-intro,
#homeSandwich[data-active-layer="top"] .sandwich-visual-panel {
  background: var(--main-lila);
  color: var(--main-hellblau);
}

#homeSandwich[data-active-layer="top"] .sandwich-intro .section-kicker {
  color: var(--main-hellblau);
}

/* Batting: blue ground with yellow text (the blue+gelb pair), reversed
   from the pale-yellow-ground/blue-text version this had before. Blue
   here matches the resting default's background, but the intro copy,
   heading, and fill word all still swap to make the hover state read
   clearly even with the same background color. */
#homeSandwich[data-active-layer="batting"],
#homeSandwich[data-active-layer="batting"] .sandwich-intro,
#homeSandwich[data-active-layer="batting"] .sandwich-visual-panel {
  background: var(--main-blue);
  color: var(--main-hellgelb);
}

#homeSandwich[data-active-layer="batting"] .sandwich-intro .section-kicker {
  color: var(--main-hellgelb);
}

/* Backing's background goes pale, so the text has to flip dark to stay
   readable -- the pale-yellow default would nearly vanish against it. */
#homeSandwich[data-active-layer="backing"],
#homeSandwich[data-active-layer="backing"] .sandwich-intro,
#homeSandwich[data-active-layer="backing"] .sandwich-visual-panel {
  background: var(--main-hellblau);
  color: var(--main-lila);
}

#homeSandwich[data-active-layer="backing"] .sandwich-intro .section-kicker {
  color: var(--main-lila);
}

/* The whole panel reacts, not just the background: the intro column's
   heading + short description swap to match the hovered layer too. */
#homeSandwich[data-active-layer] .sandwich-intro-item.is-default {
  opacity: 0;
  visibility: hidden;
}

#homeSandwich[data-active-layer="top"] .sandwich-intro-item[data-layer="top"],
#homeSandwich[data-active-layer="batting"] .sandwich-intro-item[data-layer="batting"],
#homeSandwich[data-active-layer="backing"] .sandwich-intro-item[data-layer="backing"] {
  position: static;
  opacity: 1;
  visibility: visible;
}

.home-closing {
  /* Back to a flat var(--paper-batting) fill -- the textured version
     (fiber dots + diagonal grain, matching .page-backing) read as a
     dashed/spotty pattern rather than "recycled paper," not what was
     wanted. Plain warm cream instead. */
  background: var(--paper-batting);
  display: grid;
  /* Was minmax(300px, 0.85fr) minmax(320px, 1fr) -- gave the photo LESS
     than half the row, noticeably smaller than .warm-gallery's photo.
     Matched to the same 1.4fr/1fr ratio used there and by .editorial-pair
     so this photo reads at the same scale as the others instead of
     shrinking just because this section's text column used to claim more
     space. */
  grid-template-columns: minmax(320px, 1.4fr) minmax(280px, 1fr);
  gap: clamp(24px, 5vw, 72px);
  align-items: center;
}

/* Recap of the (already-held) "It's QUILTING TIME" workshop -- an
   archive record, not an invitation, so this stays on the page's
   plain paper ground rather than a loud color block: only the
   heading picks up the lila accent, everything else (date, place,
   quilters, body copy) reads as calm black text. The video is the
   animated version of the same printed poster used at the workshop
   (see .workshop-trace below); contain (not cover) so none of the
   poster's own text is ever cropped, and no border/frame around it --
   it just sits on the page. */
.workshop-invite {
  display: grid;
  grid-template-columns: minmax(280px, 0.9fr) minmax(320px, 1.1fr);
  /* Widened from clamp(20px, 3.5vw, 48px) -- read as too tight next to
     the large video, same fix as .social-note-video below. */
  gap: clamp(28px, 4.5vw, 72px);
  padding: clamp(28px, 4.5vw, 64px) max(20px, 5vw);
  background: var(--paper);
  color: var(--ink);
}

/* No frame/border on purpose -- the video sits directly on the section's
   own lila ground rather than looking boxed-in behind glass. */
.workshop-invite-media {
  display: grid;
  place-items: center;
  overflow: hidden;
}

.workshop-invite-video {
  display: block;
  width: 100%;
  height: 100%;
  max-height: 74svh;
  object-fit: contain;
}

.workshop-invite-copy {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 12px;
  padding: clamp(10px, 2vw, 26px);
}

.workshop-invite-copy h2 {
  margin: 0;
  font-size: var(--text-h2-compact);
}

/* A small fact sheet -- label/value rows like an archive report card,
   rather than a loose stack of paragraphs that all read the same weight. */
.workshop-report {
  display: flex;
  flex-direction: column;
  margin: 6px 0 0;
}

.workshop-report-row {
  display: grid;
  grid-template-columns: minmax(84px, 112px) 1fr;
  gap: 14px;
  padding: 11px 0;
  border-top: 1px solid var(--line);
}

.workshop-report-row:first-child {
  border-top: none;
  padding-top: 0;
}

.workshop-report dt {
  margin: 0;
  padding-top: 0.15em;
  font-family: var(--mono-font);
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.55;
}

.workshop-report dd {
  max-width: 42ch;
  margin: 0;
  font-family: var(--body-font);
  color: var(--ink);
  line-height: 1.4;
}

/* Real workshop trace: photos from the last session plus a handful of
   the hand-sewn letters held individually, spelling out the same word
   the rest of the site keeps returning to in pixel-block type. Sits on
   the calm neutral paper the rest of the page uses -- the loud color
   moment above is where this page wants attention to land first. */
.workshop-trace {
  padding: clamp(48px, 7vw, 96px) max(24px, 6vw);
  background: var(--paper-top);
  border-bottom: 1px solid var(--line);
}

/* Text on the left, gallery on the right -- a stacked single column left
   a wide band of empty paper beside the (narrow) paragraph, so the two
   pieces now share the row and both put the section's full width to use. */
.workshop-trace-body {
  display: grid;
  grid-template-columns: minmax(240px, 0.8fr) minmax(360px, 1.3fr);
  /* Kicker-top/body-bottom pin retired here too -- see .pair-copy's
     comment for the site-wide reasoning. Was align-items: start, which
     left this text block pinned to the gallery's top edge with a large
     empty gap below it instead of sitting centered beside it. */
  align-items: center;
  gap: clamp(24px, 4vw, 64px);
}

.workshop-trace-head {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.workshop-trace-head p:last-child {
  max-width: 46ch;
  margin: 0;
  font-size: var(--text-base);
  line-height: 1.35;
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

/* One photo at a time instead of a seven-up grid -- click-through via
   the arrows/dots, or it auto-advances on its own, so this reads as a
   compact little viewer rather than a tall wall of thumbnails eating
   the page. All figures stack in the same box (absolutely positioned,
   cross-fading via opacity) and the inline script below toggles which
   one is .is-active. */
.workshop-gallery {
  width: 100%;
}

.workshop-gallery-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--paper-2);
}

.workshop-gallery-frame figure {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  /* Slower, gentler cross-fade (was 0.6s/4.5s) -- a subtle background
     shift rather than something that visibly "clicks" from one photo to
     the next every few seconds. */
  transition: opacity 1.4s ease-in-out;
  pointer-events: none;
}

.workshop-gallery-frame figure.is-active {
  opacity: 1;
  pointer-events: auto;
}

.workshop-gallery-frame img,
.workshop-gallery-frame video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--ink);
}

/* Caption-style tag on the one video slide, standing in for a
   <figcaption> (the site avoids those -- see task history) while still
   naming what it is before a visitor commits to pressing play. */
.workshop-gallery-video::before {
  content: "Workshop Insight";
  position: absolute;
  z-index: 1;
  top: 12px;
  left: 12px;
  padding: 3px 9px;
  background: var(--main-lila);
  color: var(--paper);
  font-family: var(--mono-font);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  pointer-events: none;
}

.workshop-gallery-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  margin-top: 14px;
}

.workshop-gallery-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--main-lila);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  transition: background-color 0.2s ease, color 0.2s ease;
}

.workshop-gallery-arrow:hover {
  background: var(--main-lila);
  color: var(--paper);
}

.workshop-gallery-dots {
  display: flex;
  gap: 7px;
}

.workshop-gallery-dot {
  width: 7px;
  height: 7px;
  padding: 0;
  border: 1px solid var(--main-lila);
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
}

.workshop-gallery-dot.is-active {
  background: var(--main-lila);
}

/* aspect-ratio (not min-height alone) on purpose -- min-height doesn't
   give the grid a definite height for the img's height:100% to resolve
   against, so the image quietly stopped short of the figure's actual
   rendered height and left the figure's own background showing as an
   unintended band beneath it. */
.home-closing figure {
  aspect-ratio: 1 / 1;
  /* Was clamp(360px, 45vw, 640px) -- bumped to match .warm-gallery
     figure's own cap, matching the wider column ratio above. */
  max-height: clamp(420px, 56vw, 760px);
}

.home-closing p:last-of-type {
  /* Nudged from 44ch to match the ~46ch measure used everywhere else this
     shape of body paragraph appears (top-photo-caption, practice-copy,
     transcript-pull). */
  max-width: 46ch;
  font-family: var(--body-font);
  /* Matches the justified/left-last blocksatz treatment every other
     standalone body paragraph on the site uses (.pair-copy, .aunt-note,
     .memory-lede, etc.) -- was left ragged-right here on both pages that
     reuse this component (homepage and beyond.html), the one paragraph
     shape that hadn't picked up the site-wide convention yet. */
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

/* This heading needs to read as exactly "Three layers," / "read one at a
   time." -- a manual <br> plus text-wrap: wrap still wasn't enough; the
   second line kept splitting into two even with plenty of box width to
   spare. Two explicit block-level spans sidestep whatever wrapping
   calculation was doing that, since each one lays out independently. */
.home-closing-line {
  display: block;
  /* Without this, `body * { font-family: var(--body-font) !important }`
     (see top of file) matches these spans directly and wins over
     inheriting Geist Mono from the parent h2's own !important rule --
     inheritance only kicks in when nothing targets the element itself,
     and the universal reset does. Raw text nodes don't have this
     problem (nothing to directly match), which is why introducing these
     wrapper spans is what flipped the heading to the body font. */
  font-family: var(--mono-font) !important;
}

.hero-copy {
  display: grid;
  grid-template-rows:
    minmax(1.2em, 1.2em)
    clamp(81px, 13.5vw, 216px)
    minmax(5.7em, 5.7em);
  align-content: end;
  row-gap: clamp(22px, 2.6vw, 38px);
  min-height: 0;
  padding: clamp(24px, 5vw, 70px) 0;
}

.hero-copy h1 {
  max-width: 10ch;
  margin: 0;
  font-family: var(--display);
  font-size: clamp(54px, 9vw, 144px);
  font-weight: 400;
  line-height: 0.5;
  letter-spacing: 0;
  text-transform: uppercase;
}

.page-top .hero-copy h1,
.page-top .hero-deck {
  color: var(--main-hellblau);
}

.page-batting .hero-copy h1,
.page-batting .hero-deck {
  color: var(--main-hellgelb);
}

.page-backing .hero-copy h1,
.page-backing .hero-deck {
  color: var(--main-lila);
}

.page-home .hero-copy h1 {
  color: var(--ink);
}

.hero-copy .eyebrow {
  margin: 0;
}

.hero-deck {
  max-width: 680px;
  min-height: 5.7em;
  margin: 0;
  font-family: var(--body-font);
  font-size: var(--text-large);
  line-height: 1.35;
  overflow: hidden;
  /* text-wrap: pretty (inherited from the global p rule) only rebalances
     the LAST line of a paragraph -- it doesn't stop a short 2-4 line
     intro like this one from breaking right after a comma or period and
     stranding just one or two words on their own line mid-paragraph
     (e.g. "...outward. Online," on beyond.html). balance instead spreads
     the whole block evenly across all its lines, which is the right
     tool for a short blurb like this (long body paragraphs elsewhere
     keep pretty, where balance would look worse over many lines). */
  text-wrap: balance;
}

.hero-media {
  min-height: 0;
  height: 100%;
  border: 1px solid rgba(23, 23, 23, 0.2);
  overflow: hidden;
  align-self: stretch;
}

.hero-media.soft img {
  filter: saturate(0.96) contrast(0.96);
}

/* The layer illustration now lives inside the hero itself (the page's one
   cover moment) instead of a separate boxed photo -- no frame, no crop,
   no caption competing with it, just the isometric graphic filling the
   bold color block as large as the space allows. */
.hero-illustration {
  display: grid;
  place-items: center;
  border: 0;
  background: transparent;
  overflow: visible;
}

.hero-illustration img {
  width: 100%;
  height: 100%;
  max-width: 1040px;
  max-height: 92%;
  object-fit: contain;
}

.top-cover-photo img {
  object-position: 50% 50%;
  filter: grayscale(1) contrast(1.04);
}

.section-pad {
  padding: clamp(76px, 10vw, 148px) max(24px, 6vw);
}

.section-kicker {
  margin: 0 0 18px;
  color: var(--blue-dark);
}

.blanket-scroll-head .section-kicker {
  /* the flex gap on .blanket-scroll-head above already spaces this from
     the paragraph -- without zeroing this out too, the two would stack. */
  margin-bottom: 0;
}

.page-top .section-kicker {
  color: var(--main-lila);
}

.page-batting .section-kicker {
  color: var(--main-blue);
}

.page-backing .section-kicker {
  color: var(--main-lila);
}

/* The hero eyebrow lives inside the bold color-block opening (see
   .top-hero / .batting-hero / .backing-hero below), so it takes the light
   accent color that pairs with that background -- the same top/batting/
   backing combo used for the homepage illustration hover. */
.page-top .hero-copy .eyebrow {
  color: var(--main-hellblau);
}

.page-batting .hero-copy .eyebrow {
  color: var(--main-hellgelb);
}

.page-backing .hero-copy .eyebrow {
  color: var(--main-lila);
}

h2 {
  margin: 0;
  font-family: var(--mono-font);
  font-size: var(--text-h2);
  font-weight: 500;
  line-height: 1.1;
  letter-spacing: 0;
  /* Redistributes line breaks across a heading so it doesn't leave a
     single stray word alone on the last line. Supported in current
     Chrome/Safari/Edge; browsers without it just wrap normally. */
  text-wrap: balance;
}

/* Same idea for body copy -- avoids a lone orphan word on its own last
   line wherever the browser supports it (text-wrap: pretty is newer/less
   supported than balance above, so this is a progressive enhancement,
   not something to rely on everywhere). */
p {
  text-wrap: pretty;
}

h3 {
  margin: 0;
  font-family: var(--mono-font);
  font-size: var(--text-h3);
  font-weight: 500;
  line-height: 1.15;
  /* Same orphan-prevention as h2 above. */
  text-wrap: balance;
}

p {
  font-size: var(--text-base);
  line-height: 1.6;
}

/* One large full-bleed photo moment right after the hero -- unlike the
   matted .editorial-pair figures further down the page, this one runs
   edge to edge on purpose, closer in scale to the hero itself, so it
   reads as a second beat of the opening rather than another paired
   photo-and-text section. The manifesto statement (kicker + heading +
   paragraph) that used to sit in its own plain section right below now
   lives directly on this photo instead -- see .top-photo-caption. */
/* Background matches the photo's own pale blue studio backdrop -- shows
   through at the edges once the image is scaled down slightly below
   (see .top-photo-feature figure img), so the "zoomed out" letterboxing
   blends into the photo instead of reading as a gap or a bug. */
.top-photo-feature {
  position: relative;
  background: var(--main-hellblau);
}

/* figure needs an explicit width here -- with only aspect-ratio + a
   max-height cap and no width set, the browser was solving the aspect
   ratio by shrinking the figure's WIDTH to match the capped height
   (since the source photo's own ratio is taller than 3/2), leaving a
   blank gap beside it instead of running edge to edge. Pinning width to
   100% keeps it full-bleed regardless of the source photo's ratio; the
   image itself now does the cropping via object-fit: cover below.
   No max-height cap -- the source photo's own ratio is already ~3:2
   (2400x1601), so at 100% width and aspect-ratio 3/2 the box matches the
   image almost exactly and object-fit: cover has nothing left to crop.
   The old 92svh cap was shorter than that on most laptop screens, which
   is what was cropping the top/bottom off the photo. */
.top-photo-feature figure {
  width: 100%;
  aspect-ratio: 3 / 2;
}

/* Was transform: scale(0.94) -- the "zoomed out" treatment that let the
   figure's pale blue background show as a margin around the photo. Reads
   as a bug (a cream/blue border and rounded-looking edge) rather than
   intentional breathing room, and doesn't match the other two
   .top-photo-feature instances (.room-feature, .backing-feature), which
   both already run edge to edge with no scale-down. Removed so all three
   are consistent: full-bleed, no border. */
.top-photo-feature figure img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Pushed further right again (48% -> 62%) -- still not far enough to
     bring the quilt's draped edge into view the way it was asked for. */
  object-position: 62% 50%;
  display: block;
}

.top-photo-feature figcaption {
  font-family: var(--body-font);
  font-style: italic;
}

/* Went through a bottom-edge linear gradient, then a soft radial "dark
   pool" anchored under the text -- both were fighting the same problem:
   a semi-transparent shadow trying to guarantee contrast against
   whatever happens to sit behind the text, which changes as the caption
   moves and as the photo's own colors vary underneath it. A solid card
   (see .top-photo-caption below) sidesteps that entirely -- guaranteed
   contrast everywhere, independent of the photo -- so this overlay is no
   longer needed. */

.top-photo-caption {
  position: absolute;
  left: max(20px, 5vw);
  /* +58px to clear .site-nav -- it's position:sticky at top:0, so once
     this section's top edge scrolls to the viewport's top edge, the nav
     sits on top of (and would otherwise cover) the first 58px of the
     section, which is exactly where this caption starts. */
  top: calc(58px + clamp(24px, 4vw, 56px));
  z-index: 2;
  max-width: 46ch;
  padding: clamp(20px, 3vw, 34px);
  /* Solid dark card (was text-shadow-on-photo) -- the shadow approach
     kept failing wherever it landed on a lighter or busier part of the
     quilt underneath. A solid backing panel guarantees legibility
     regardless of what photo sits behind it, and reads as a deliberate
     "label" on the photo rather than text fighting to stay visible.
     Slight blur softens the edge against the image instead of a hard
     rectangle cutout. Was 0.72 -- nudged down slightly per request, still
     dark enough to guarantee text contrast over any photo underneath. */
  background: rgba(23, 23, 23, 0.62);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

.top-photo-caption .section-kicker {
  color: var(--main-hellblau);
}

/* Extra .top-photo-feature ancestor bumps specificity above the page-wide
   .page-top h2 rule below (same specificity otherwise, so source order
   would decide) -- this one needs to stay white for contrast over the
   dark card regardless of where that rule sits in the file. */
/* Compact size (was the full --text-h2, up to 64px) -- at that size
   "More than a textile." itself was wrapping mid-phrase inside the
   caption's max-width, on top of the intentional <br> before "A way of
   living.", so the line was breaking three ways instead of two. */
.top-photo-feature .top-photo-caption h2 {
  margin: 10px 0 16px;
  color: var(--paper);
  font-size: var(--text-h2-compact);
}

.top-photo-caption p:last-child {
  margin: 0;
  font-family: var(--body-font);
  /* --text-base -- unifying this with every other body paragraph on the
     site (was the last --text-large straggler still reading noticeably
     bigger than its neighbors). */
  font-size: var(--text-base);
  line-height: 1.5;
  color: var(--paper);
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

/* .room-feature reuses the .top-photo-feature/.top-photo-caption pattern
   wholesale (full-bleed photo, dark overlay card) for the "In the Room"
   section on the Batting page -- same box, same structure, only the
   photo's own crop and the kicker's accent color differ per instance.
   Replaced the old portrait sleeping-under-quilt.jpg (1333x2000) with a
   landscape shot of two people under a lifted quilt "tent" -- the
   portrait source needed a tight 5:4 crop that read as mostly hair/face
   with the quilt reduced to backdrop; this new photo is already ~3:2
   (4603x3069), same as the shared .top-photo-feature figure ratio, so no
   aspect-ratio override is needed here anymore and the quilt itself (the
   whole tent shape, plus more quilts in the background) stays clearly
   the subject instead of being cropped down to a sliver. */
.room-feature figure img {
  object-position: 50% 42%;
}

.room-feature-caption .section-kicker {
  color: var(--main-hellgelb);
}

/* Same .top-photo-feature/.top-photo-caption pattern for Backing's own
   opening photo beat -- Top and Batting both open this way already, this
   was the one page missing it. Source photo (8688x5792) is already ~3:2
   like the shared figure aspect-ratio, so no crop override needed; only
   the kicker color changes, matching Backing's lila accent. */
.backing-feature-caption .section-kicker {
  color: var(--main-lila);
}

/* The shared base rule's object-position: 62% 50% was tuned for Top's
   own star-quilt-drape photo (bringing its draped edge into frame) --
   left un-overridden here, Backing's workshop-poster photo inherited that
   same rightward shift too, cropping its own "QUILTING" cube logo off to
   one side instead of centered in view. Plain center instead. */
.backing-feature figure img {
  object-position: 50% 50%;
}

.manifesto-grid,
.intro-grid {
  display: grid;
  grid-template-columns: minmax(280px, 0.95fr) minmax(300px, 1.05fr);
  gap: clamp(28px, 5vw, 78px);
  align-items: end;
}

.manifesto-grid p {
  margin: 0 0 1em;
  font-family: var(--body-font);
  font-size: var(--text-base);
  line-height: 1.55;
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

/* Smaller than the base h2 clamp so "More than a textile." fits on one
   line within this grid's narrower left column instead of wrapping mid-
   sentence -- the explicit <br> in the markup then reliably lands the
   second sentence on its own line too. */
.manifesto h2 {
  font-size: var(--text-h2-compact);
}

/* White background throughout -- lila is reserved for the heading only
   now, not a full-bleed color block. The bold lila/hellblau pairing here
   read as too loud; color should stay a subtle accent (headings, kickers,
   pull-quotes), not a background treatment. */
.timeline-section {
  display: grid;
  /* minmax(0, 1fr) on the card side, not a bare 1fr: the square
     .timeline-card boxes carry aspect-ratio, and aspect-ratio combined
     with a grid item's default min-width:auto produces a large automatic
     content minimum that was winning over this track's fr-share and
     shoving the intro sidebar down to its 280px floor. Capping the
     track's own minimum at 0 hands sizing back to the fr proportion. */
  grid-template-columns: minmax(280px, 0.36fr) minmax(0, 1fr);
  /* Wider than the standard clamp(14px,2vw,28px) 2-column gap used
     elsewhere -- this intro column sits beside a whole 3-up photo grid
     rather than a single photo, so the old narrower gap read as the text
     crowding straight into the cards. */
  gap: clamp(32px, 5vw, 72px);
  padding: clamp(28px, 5vw, 72px) max(20px, 5vw);
  background: var(--paper);
  color: var(--ink);
}

.timeline-intro {
  position: sticky;
  top: 70px;
  align-self: start;
  min-height: calc(100svh - 94px);
  display: flex;
  flex-direction: column;
  /* Kicker-top/body-bottom pin retired here too -- see .pair-copy's
     comment for the site-wide reasoning. This sidebar keeps its sticky
     min-height (so it stays put while the photo grid scrolls past
     beside it) but the kicker/heading/paragraph now sit together as one
     centered block instead of forced to the two far ends. */
  justify-content: center;
  gap: 16px;
  /* No top padding -- .timeline-section's own top padding already sets
     this column's top edge; adding more here on top of that pushed the
     kicker noticeably lower than the photo grid beside it. Bottom
     padding only, so the last paragraph still gets some breathing room
     above the section's own bottom edge. Horizontal padding stays off
     for the same reason: it would stack on .timeline-section's own left
     padding and break the equal-margin grid look. */
  padding-bottom: clamp(24px, 4vw, 54px);
  background: var(--paper);
  color: var(--ink);
}

.timeline-intro .section-kicker {
  color: var(--main-lila);
}

.timeline-intro p:last-child {
  /* This cap only needs to be narrow while .timeline-intro is a slim
     sticky sidebar (see the >1000px layout below); once the section
     drops to a single stacked column the cap is lifted (see media
     queries) so justify has enough width per line to avoid the big
     gappy rivers a too-narrow measure produces. */
  max-width: 38ch;
  /* Spacing from the heading above now comes from .timeline-intro's own
     flex gap, not a margin here -- kept as margin before the
     kicker-top/body-bottom pin was retired (see .timeline-intro). */
  margin: 0;
  font-family: var(--body-font);
  /* Was missing, so this fell back to the smaller global `p` default
     instead of the --text-base size every other standalone intro
     paragraph on the site now uses (see .memory-lede p:last-of-type
     etc. -- shrunk from --text-large so the Top page's body copy reads
     at the same size as the interview transcript on Batting). */
  font-size: var(--text-base);
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

.timeline-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  /* min-content instead of the implicit auto: auto-sized row tracks were
     computing a couple pixels taller than the aspect-ratio square cards
     actually need, so the vertical gap between rows read visibly larger
     than the 12px horizontal gap between columns even though both used
     the same gap value -- min-content sizes each row to the cards' own
     height instead. */
  grid-auto-rows: min-content;
  gap: 12px;
}

.timeline-card {
  /* Square identity, matching the rest of the site's quilt-block imagery
     (see the site-wide square-format convention): the card's box locks to
     a 1:1 ratio off the grid column's own width instead of a fixed
     min-height, so it stays square at any viewport instead of stretching
     taller than it is wide. */
  aspect-ratio: 1;
  min-width: 0;
  perspective: 1200px;
  -webkit-perspective: 1200px;
  outline: none;
  cursor: pointer;
}

.timeline-card:focus {
  outline: 2px solid var(--main-hellblau);
  outline-offset: 3px;
}

.timeline-card-inner {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  -webkit-transform-style: preserve-3d;
  transition: transform 520ms cubic-bezier(0.2, 0.7, 0.2, 1);
  /* Forces its own GPU compositing layer -- without this, iOS Safari has
     a long-standing bug where backface-visibility:hidden on the child
     faces (below) isn't reliably honored during/after a 3D rotation,
     letting the face that should be hidden show through as a mirrored
     ghost behind the visible one (reported: "SURVIVAL / Protection
     before Comfort" bleeding through, reversed, at the bottom of the
     flipped card). Promoting this element to its own layer up front
     makes Safari composite the two faces properly. */
  will-change: transform;
}

.timeline-card:hover .timeline-card-inner,
.timeline-card:focus .timeline-card-inner,
.timeline-card:focus-within .timeline-card-inner {
  transform: rotateY(180deg);
}

/* The will-change/-webkit- prefixed fix above still isn't enough on some
   real iOS Safari devices -- the "ghost" (a reversed, bled-through copy
   of the back face's text) has been reported again after that fix
   shipped, so this is a known-stubborn Safari bug with the
   backface-visibility + preserve-3d combination itself, not something a
   GPU-layer hint reliably resolves in every case. Rather than keep
   chasing it, touch-only devices (hover: none -- the ones that were
   actually seeing this, since flipping there is triggered by tap/focus,
   not a real hover) skip the 3D transform pipeline entirely: both faces
   sit in the same flat plane and swap via a plain opacity crossfade
   instead, driven by .is-flipped (toggled on tap -- see
   initTimelineCardTap() in site-effects.js). No rotation, no backface-
   visibility, no compositing quirk to trigger in the first place. Mouse/
   keyboard users are untouched -- they still get the original 3D flip. */
@media (hover: none) {
  .timeline-card-inner {
    transform: none !important;
  }

  .timeline-face {
    transition: opacity 260ms ease;
  }

  .timeline-face.timeline-front {
    opacity: 1;
    z-index: 2;
  }

  .timeline-face.timeline-back {
    /* Cancel the rotateY(180deg) that's baked into .timeline-back for the
       3D version -- flat and reversed would read as mirrored text. */
    transform: none;
    opacity: 0;
    z-index: 1;
  }

  .timeline-card.is-flipped .timeline-face.timeline-front {
    opacity: 0;
    z-index: 1;
  }

  .timeline-card.is-flipped .timeline-face.timeline-back {
    opacity: 1;
    z-index: 2;
  }
}

.timeline-face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 20px;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  overflow: hidden;
  background:
    linear-gradient(135deg, rgba(255, 255, 255, 0.78), rgba(255, 255, 255, 0.18)),
    var(--paper);
  color: var(--ink);
}

.timeline-face.history-photo {
  justify-content: flex-end;
  padding: 0;
  background: var(--paper-top);
}

.timeline-face.history-photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(0.92) contrast(1.02);
}

/* Placeholder used where a real photograph hasn't been supplied yet -- a
   plain muted fill instead of guessing at (or sourcing without rights)
   a stand-in image. Swap the figure back to a normal history-photo with
   an <img> once a photo is available. */
.timeline-face.history-photo.is-placeholder {
  display: flex;
  align-items: flex-end;
  background: var(--paper-2);
  border: 1px solid rgba(23, 23, 23, 0.14);
}

.timeline-face.history-photo.is-placeholder figcaption {
  background: transparent;
}

.timeline-face.history-photo figcaption {
  position: relative;
  z-index: 2;
  align-self: flex-start;
  display: inline-grid;
  gap: 2px;
  margin: 0;
  padding: 7px 9px;
  background: rgba(255, 253, 250, 0.82);
}

.timeline-face.history-photo figcaption span {
  font-size: 9px;
}

.timeline-face.history-photo figcaption strong {
  max-width: none;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.15;
}

.timeline-front::after,
.timeline-back::after {
  content: "";
  position: absolute;
  right: 18px;
  top: 18px;
  width: 42%;
  aspect-ratio: 1;
  background: var(--blue-dark);
  opacity: 0.18;
  -webkit-mask: var(--timeline-mask) center / contain no-repeat;
  mask: var(--timeline-mask) center / contain no-repeat;
}

.timeline-face.history-photo::after,
.timeline-face.history-note::after {
  content: none;
}

.timeline-back {
  transform: rotateY(180deg);
  background:
    radial-gradient(circle at 82% 18%, rgba(255, 248, 239, 0.64), transparent 30%),
    var(--butter);
}

.page-top .timeline-back {
  background:
    radial-gradient(circle at 82% 18%, rgba(255, 253, 250, 0.64), transparent 30%),
    var(--main-hellblau);
}

.timeline-face.history-note {
  padding: clamp(20px, 2.6vw, 32px);
  background: var(--paper-top);
  border: 1px solid rgba(23, 23, 23, 0.16);
}

.timeline-grid span {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.timeline-grid strong {
  display: block;
  max-width: 14ch;
  font-family: var(--body-font);
  font-size: clamp(20px, 2.2vw, 34px);
  font-weight: 500;
  line-height: 1.05;
}

/* The back-face category label sits flush at the top of the flex column
   (space-between on .timeline-face); pushing the title down with an auto
   margin above it -- instead of letting space-between split the gap evenly
   with the note below -- keeps the title close to its note paragraph and
   moves the empty air to sit above the title, matching the reference. */
.timeline-back strong {
  margin-top: auto;
  margin-bottom: 14px;
}

.timeline-grid p {
  position: relative;
  z-index: 1;
  margin: 0;
  font-size: 13px;
  /* Left-aligned, not justified -- these cards are square and narrow
     (only a few words per line), so justify was stretching word spacing
     into visibly uneven gaps/rivers on nearly every line. Justify reads
     fine on the site's wider paragraph columns elsewhere, but doesn't
     have enough words per line here to distribute evenly. */
  text-align: left;
  hyphens: none;
}

.timeline-grid span,
.timeline-grid strong {
  position: relative;
  z-index: 1;
}

.section-heading {
  display: grid;
  grid-template-columns: minmax(180px, 0.44fr) 1fr;
  gap: 28px;
  align-items: end;
  margin-bottom: 32px;
}

.practice-quilt {
  display: grid;
  grid-template-columns: minmax(320px, 1.12fr) minmax(280px, 0.88fr);
  gap: clamp(20px, 3vw, 48px);
  background: var(--paper-top);
  border-bottom: 1px solid var(--line);
  /* Vertically centers the whole kicker+heading+body block beside the
     photo instead of pinning kicker to the top and body to the bottom
     with a gap forced between -- matches .community-result-grid's
     simpler, calmer typography, which reads better on short copy next
     to a tall photo. */
  align-items: center;
}

/* Back to a fixed square (was briefly grid-stretched to fix a
   responsive alignment bug -- see the old note this replaced -- but the
   user asked for the square crop back). This reopens that alignment
   risk on narrow viewports where .practice-copy wraps taller than the
   square photo; accepted the same way as .social-note-video's return to
   a fixed aspect-ratio. max-height stays as a ceiling. */
.practice-quilt figure {
  position: relative;
  overflow: hidden;
  aspect-ratio: 1 / 1;
  max-height: min(78svh, 780px);
}

.practice-quilt figure img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* No border/accent-bar box here on purpose -- matching .pair-copy below,
   which sits on plain paper with no frame. A boxed border only earns its
   keep where it's doing a job (e.g. figure captions); on a plain text
   column it read as an arbitrary color bar with no reason behind it. */
/* Horizontal-only padding kept for the reason noted below (matches
   .practice-quilt's own left inset); the flex-column + kicker-pinned-top
   treatment is gone in favor of vertical centering via the parent
   grid's align-items: center -- see .pair-copy's comment for the full
   reasoning (same pattern, applied here too). */
.practice-copy {
  /* .timeline-intro drops its own side padding for the same reason: it
     would stack on top of .practice-quilt's own left inset and nudge
     this column further from the edge than the figure beside it. */
  padding-block: clamp(24px, 4vw, 54px);
  background: var(--paper-top);
}

.practice-copy h2 {
  margin-bottom: clamp(30px, 3.6vw, 46px);
  font-size: var(--text-h2-compact);
}

.practice-copy p:not(.section-kicker) {
  max-width: 46ch;
  margin: 0 0 18px;
  font-size: var(--text-base);
  line-height: 1.55;
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

/* The word list itself, run as one justified field rather than a bullet
   grid -- at this density (50 short words) a prose block reads more like
   the "visual language" the page keeps describing than a UI-style tag
   list would. No label above it -- just the block itself, sized to a
   near-square so it reads as a page from Kim Minseon's own book rather
   than a wide banner running edge to edge. */
.practice-vocabulary {
  /* Was clamp(64px, 10vw, 140px) -- with the column narrowed back down
     below, the block reads noticeably shorter/wider than tall unless the
     section itself also gives it more room top and bottom; bumped both
     ends of the clamp so the block never feels squeezed against the
     border-top/border-bottom rules. */
  padding: clamp(96px, 13vw, 180px) max(20px, 5vw);
  background: var(--paper-batting);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

/* Blocksatz stays (site convention for this kind of standalone
   statement) -- was max-width: 31ch, narrow enough that each line only
   held 3-6 of these words, so justify stretched the spacing between them
   hard to fill the line, and short lines (2-3 words) got huge, uneven
   gaps next to fuller ones right above/below. 64ch fixed the gap problem
   but made the block read as a wide shallow banner. 46ch was closer but
   still landed as a ~2.5:1 rectangle, not the actual square that was
   asked for. Narrower still (33ch) wraps this word count into roughly as
   many lines as the column is wide -- a true square block, not just a
   centered rectangle -- while text-align-last: center still keeps the
   last line from stranding at an edge. */
.practice-vocabulary p {
  max-width: 33ch;
  margin: 0 auto;
  font-family: var(--mono-font);
  font-size: var(--text-large);
  line-height: 1.8;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  text-align: justify;
  /* Was center -- centering the last line left it narrower than every
     other (fully-justified) line above it, so the block's right/left
     edges stepped in right at the bottom instead of staying flush --
     exactly what broke the square silhouette. text-align-last: justify
     stretches even a short last line (word-spacing) to the same full
     width as the rest, so the block reads as one solid rectangle again. */
  text-align-last: justify;
  /* Tried balance here to even out the last line further, but at this
     block's ~10 lines it made things worse -- Chromium's balance
     heuristic degrades past a handful of lines and left a single word
     ("TECHNIQUE") alone on its own line, a worse orphan than the two-word
     line pretty produces. Back to pretty, which specifically avoids
     single-word orphan lines as one of its heuristics. */
  text-wrap: pretty;
  hyphens: none;
  color: var(--ink);
}

.practice-texture-pair {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(14px, 2vw, 28px);
  padding: clamp(28px, 5vw, 72px) max(20px, 5vw);
  background: var(--paper);
}

.practice-texture-pair figure {
  aspect-ratio: 1 / 1;
}

/* Full-bleed on purpose, like .top-photo-feature near the hero -- the
   crumpled, from-above shot is the "movement" the closing statement
   below talks about, so it gets the same larger, edge-to-edge scale
   instead of being boxed down to match the matted photos around it. */
.practice-movement {
  padding: 0;
}

.practice-movement figure {
  aspect-ratio: 1 / 1;
  max-height: 92svh;
}

.practice-movement figcaption {
  font-family: var(--body-font);
  font-style: italic;
}

/* Closing statement for the practice-quilt piece -- background matches
   the hellblau letterbox already sitting behind that quilt's own photo,
   so this reads as an epilogue to the section above rather than an
   unrelated new block. */
.practice-close {
  padding: clamp(48px, 8vw, 96px) max(20px, 5vw);
  background: var(--main-hellblau);
  text-align: center;
}

.practice-close p {
  max-width: 34ch;
  margin: 0 auto;
  font-family: var(--body-font);
  font-size: var(--text-quote);
  font-weight: 600;
  font-style: italic;
  line-height: 1.4;
  color: var(--ink);
}

.editorial-pair,
.sensory,
.warm-gallery {
  display: grid;
  gap: clamp(14px, 2vw, 28px);
  padding: clamp(28px, 5vw, 72px) max(20px, 5vw);
}

/* Own gap instead of sharing .sensory/.warm-gallery's -- this is one of
   the photo-plus-copy pair sections (matching .aunt-archive, .tempo-note,
   .social-note, .transcript-intro), which all now use the same wider gap
   so the photo and its text read as consistently spaced apart no matter
   which paired section you're scrolling past. */
.editorial-pair {
  /* Was minmax(320px, 1.05fr) minmax(320px, 0.95fr) -- close to an even
     split. Matched to .warm-gallery's own 1.4fr/1fr ratio below so the
     photo reads at the same scale/prominence in both sections instead of
     looking smaller here. */
  grid-template-columns: minmax(320px, 1.4fr) minmax(280px, 1fr);
  gap: clamp(20px, 3vw, 48px);
  background: var(--paper-top);
  /* Same reasoning as .practice-quilt above -- vertical centering
     instead of top/bottom pinning. */
  align-items: center;
  /* Matches .transcript's established 1720px cap -- without this the
     grid ran edge-to-edge on wide monitors, growing the photo column far
     beyond what .pair-copy's own 620px text cap could fill. That left a
     large dead-space gap between photo and text plus a lot of unused
     width past the text block, reading as "text too narrow" even though
     the text itself hadn't changed. */
  max-width: 1720px;
  margin-left: auto;
  margin-right: auto;
}

.sensory {
  grid-template-columns: repeat(3, 1fr);
}

/* Down to a single photo (was 3, two of which were nearly the same quilt
   re-shot on a different backdrop -- see in.html) shown large, paired
   with a short quote instead of a third thumbnail competing for the same
   attention. */
.warm-gallery {
  grid-template-columns: minmax(320px, 1.4fr) minmax(240px, 1fr);
  align-items: center;
}

figure {
  position: relative;
  margin: 0;
  overflow: hidden;
  background: var(--paper-2);
}

figcaption {
  position: absolute;
  left: 10px;
  bottom: 10px;
  padding: 7px 9px;
  background: rgba(255, 248, 239, 0.88);
  color: var(--ink);
}

/* The bold top/batting/backing color-blocking is a hero-only device (see
   .top-hero / .batting-hero / .backing-hero above). Everywhere else on the
   page -- this photo caption, the editorial-pair copy block -- stays calm
   and neutral, so the loud pairing keeps reading as "the start of the
   page" instead of repeating throughout. */
/* Was aspect-ratio: 1/1 -- fixed to the column's own width, independent
   of .pair-copy next to it. On a narrower viewport the copy column wraps
   to more lines (taller) while the square photo's own width (and so its
   height) shrinks, so the grid row grows to fit the taller text but the
   aspect-ratio-locked photo can't grow with it -- the photo's bottom
   edge stops short of the copy's, breaking the top/bottom-pinned match
   the two are supposed to share (confirmed live: monitor vs. laptop
   screenshots of this exact section showed the mismatch). Removing the
   aspect-ratio lets the figure (a grid item with the browser default
   align-items: stretch) always take on the row's actual height --
   whichever column is taller at that viewport -- so it tracks .pair-copy
   exactly at every width instead of only when the row happens to be
   photo-driven. img fill is handled below via position:absolute so the
   percentage-height doesn't depend on the figure's height being
   "definite" the way normal-flow sizing needs -- an earlier note on
   .home-closing found that a plain height:100% img can quietly stop
   short of a grid-stretched figure's actual box; absolute + inset:0
   resolves against the figure's own final size instead, which is the
   more reliable path. */
.editorial-pair figure {
  position: relative;
  overflow: hidden;
}

/* One specific .editorial-pair (the diamond-quilt "Contemporary
   Relevance" section on through.html) opted back into a fixed square
   crop per the user's request, while the archive-photo .editorial-pair
   above it keeps the stretch fix. The img fill below still works
   unchanged since it fills via absolute inset:0 regardless of whether
   the figure's own height comes from aspect-ratio or from grid stretch. */
.editorial-pair--square figure {
  aspect-ratio: 1 / 1;
  /* Matches .warm-gallery figure's own cap so this photo reads at the
     same scale, not smaller just because this section's column ratio
     used to be closer to even. */
  max-height: clamp(420px, 56vw, 760px);
}

.editorial-pair figure img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* flex-start + the last-child rule below (not center) so the kicker lines
   up with the figure's own top edge and the closing text lines up with
   its bottom edge, instead of floating as one centered block with empty
   space above and below -- same principle as .practice-copy, now applied
   here too. */
/* Kicker-top/body-bottom pinning (the previous approach here) is
   retired in favor of the calmer .community-result-head pattern: no
   flex column, no auto-margin pin, no forced gap between the kicker and
   the rest -- just plain stacked children, vertically centered as one
   block via the parent grid's align-items: center (see .editorial-pair
   above). Capped width + auto side margins stay, since that's a
   separate fix (balancing left/right whitespace on short copy). */
.pair-copy {
  padding: clamp(24px, 4vw, 54px);
  background: var(--paper);
  color: var(--ink);
  /* Was max-width: 480px -- read as too narrow/cramped next to the photo
     column (archive-photograph and contemporary-relevance sections both
     flagged), leaving a lot of unused width in the grid cell. Widened so
     the text actually uses the space the grid gives it. */
  max-width: 620px;
  margin-left: auto;
  margin-right: auto;
}

.pair-copy h2 {
  font-size: var(--text-h2-compact);
  margin-bottom: clamp(30px, 3.6vw, 46px);
}

/* Batting headings read dark blue throughout, matching the page's
   established accent (kicker and link are already --main-blue) instead of
   defaulting to plain ink or borrowing the lila used on the other pages'
   .pair-copy h2. The nested selector outranks .pair-copy h2 above since
   both are otherwise equal specificity. */
.page-batting h2 {
  color: var(--main-blue);
}

.page-batting .pair-copy h2 {
  color: var(--main-blue);
}

/* Same page-wide "one accent color for every heading" rule as
   .page-batting above, extended to the other two pages. Previously only
   some headings on a page were colored (pair-copy, timeline-intro,
   workshop/tool/gallery sections) while others (manifesto-grid,
   practice-copy) stayed plain ink -- an arbitrary-looking switch between
   colored and neutral headings within the same page. One rule per page,
   no exceptions, matching page-batting's existing convention. */
.page-top h2,
.page-backing h2 {
  color: var(--main-lila);
}

/* Homepage was the one page left out of the "one accent color for every
   heading" convention above -- its kickers and h2s still fell back to
   plain ink while every other page had committed to a single accent.
   Blue matches the homepage's kicker (.section-kicker already defaults to
   --blue-dark) so title and subtitle now read as one consistent color. */
.page-home h2 {
  color: var(--main-blue);
}

/* .home-sandwich is its own bold color block (blue/lila/hellblau ground
   swapping per hovered layer, with hellgelb/hellblau/lila text to match)
   and its h2s were never meant to join the page-wide blue-heading
   convention above -- doing so made every heading in here blue-on-blue
   at rest and frozen through every hover color swap. Higher specificity
   than .page-home h2 lets these keep inheriting color from
   .sandwich-intro/#homeSandwich[data-active-layer] the way they did
   before that page-wide rule existed. */
.page-home .home-sandwich h2 {
  color: inherit;
}

/* :not(.section-kicker) -- otherwise this plain "p" selector's extra
   specificity (class+tag) beats .section-kicker's own font-size rule
   (class alone), silently blowing the kicker up to this paragraph's
   --text-large size (22px) instead of the intended --text-xs (12px) --
   the exact bug already fixed once for .aunt-note p, now applied
   everywhere else the same shape of container repeats it. */
.pair-copy p:not(.section-kicker) {
  margin: 0 0 24px;
  font-family: var(--body-font);
  font-size: var(--text-base);
  line-height: 1.55;
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

/* Concrete numbers next to the poetic heading -- same instinct as the
   blanket captions' cm dimensions: a lyrical line reads stronger once
   something measurable sits beside it. */
.time-stat {
  /* Was 4px -- read as glued directly onto the paragraph above it, no
     breathing room before the "Measured in Time" label starts a new
     idea. */
  margin-top: clamp(32px, 4.5vw, 52px);
}

.time-stat .section-kicker {
  margin-bottom: 14px;
}

.time-stat-list {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(18px, 3vw, 34px);
  margin: 0;
  padding: 0;
  list-style: none;
}

.time-stat-list li {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.time-stat-list strong {
  font-family: var(--mono-font);
  font-size: clamp(22px, 2.4vw, 32px);
  font-weight: 700;
  color: var(--main-blue);
  line-height: 1;
}

.time-stat-list span {
  font-family: var(--mono-font);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.65;
}

.text-link {
  align-self: flex-start;
  padding: 11px 13px;
  border: 1px solid currentColor;
  text-decoration: none;
}

.page-top .text-link,
.page-backing .text-link {
  color: var(--main-lila);
}

.page-batting .text-link {
  color: var(--main-blue);
}

.page-home .text-link {
  color: var(--main-blue);
}

/* Plain solid invert on hover -- the button's own border/text color fills
   in as the background and the label flips to paper, instead of swapping
   to a second accent color. Simpler than the earlier lila+hellblau /
   blue+gelb pairs. Base rule covers any .text-link outside those three
   page contexts (e.g. the home page's "Open the digital backing" link,
   which otherwise has no page-specific color and just inherits ink). */
.text-link:hover {
  background: var(--ink);
  color: var(--paper);
}

.page-top .text-link:hover,
.page-backing .text-link:hover {
  background: var(--main-lila);
  color: var(--paper);
}

.page-batting .text-link:hover {
  background: var(--main-blue);
  color: var(--paper);
}

.page-home .text-link:hover {
  background: var(--main-blue);
  color: var(--paper);
}

.memory-lede {
  max-width: 980px;
}

/* One canonical body-paragraph treatment (justified/left-last blocksatz)
   now used for every standalone intro/statement paragraph site-wide --
   previously this varied section to section (some justified, some
   ragged-left; some at --text-large, some at the smaller default), which
   read as an inconsistent type scale when pages were compared side by
   side. Still used by .tempo-note and .social-note on this page, even
   though the stats/cards block that used to sit here too now lives
   inside .aunt-archive above.
   Down to --text-base (was --text-large) -- matching the interview
   transcript's own body size, and directly shrinking how much vertical
   space this paragraph needs against a paired photo/video whose own
   height is fixed (most visible on .social-note, where a 16:9 video in
   its narrower column is nowhere near as tall as this text block used
   to run). */
.memory-lede p:last-of-type {
  /* Was 720px -- wider than the 620px cap the rest of the photo-plus-copy
     family now shares (.pair-copy, .aunt-note, .transcript-intro-copy);
     never actually the binding constraint in practice since the grid
     column itself was usually narrower, but aligned now so the number
     itself is consistent site-wide, not just the rendered result. */
  max-width: 620px;
  font-family: var(--body-font);
  font-size: var(--text-base);
  line-height: 1.55;
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

.memory-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  margin-top: 42px;
}

.memory-cards article {
  position: relative;
  min-height: 330px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 28px 22px 22px;
  background: rgba(255, 253, 250, 0.52);
  border: 1px solid rgba(23, 23, 23, 0.14);
  overflow: hidden;
}

.memory-cards article::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 8px;
  background: linear-gradient(90deg, var(--main-blue), var(--main-hellgelb));
}

.memory-cards article:nth-child(2)::before {
  background: linear-gradient(90deg, var(--main-hellblau), var(--main-lila));
}

.memory-cards article:nth-child(3)::before {
  background: linear-gradient(90deg, var(--main-blue), var(--main-hellgelb));
}

.memory-cards span {
  color: var(--main-blue);
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.12em;
}

.memory-cards article:nth-child(2) span {
  color: var(--main-lila);
}

.memory-cards p,
.sensory-copy p,
.interview p,
.next-layer p {
  font-family: var(--body-font);
}

/* This section now carries what used to be two separate sections: the
   portrait+copy pairing (photo-plus-copy, matching .editorial-pair and
   .tempo-note-photo elsewhere on this page) plus the stats/quote-cards
   block that used to sit in its own .memory-section above it. Combining
   them keeps this as one continuous statement about her practice --
   speed, time and the refusal of a machine -- instead of splitting it
   across two back-to-back sections making the same point. The cards
   span both columns via .aunt-archive .memory-cards below. */
/* Column ratio swapped alongside the markup order below (.aunt-note now
   first/left, .aunt-portrait second/right) so the photo keeps its
   original, slightly-larger 1.05fr share -- only the left/right position
   changed, not which column reads as the "bigger" one. */
.aunt-archive {
  display: grid;
  grid-template-columns: minmax(320px, 0.95fr) minmax(320px, 1.05fr);
  gap: clamp(20px, 3vw, 48px);
  padding: clamp(28px, 5vw, 72px) max(20px, 5vw);
  background: var(--paper-batting);
  align-items: center;
}

.aunt-archive .memory-cards {
  grid-column: 1 / -1;
  margin-top: clamp(28px, 4vw, 44px);
}

.aunt-archive figure {
  min-height: 0;
}

/* Same fix as .editorial-pair figure above -- was aspect-ratio: 1/1,
   which stopped tracking .aunt-note's actual height once its text wrapped
   to more lines on a narrower screen. No aspect-ratio now, so grid
   stretch always sizes this to the row's real height; the img fills it
   via position:absolute (see .editorial-pair figure img comment for why
   that's more reliable here than a plain height:100%). */
.aunt-portrait {
  position: relative;
  overflow: hidden;
  aspect-ratio: 1 / 1;
}

.aunt-portrait img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Same top/bottom-pinned principle as .pair-copy and .practice-copy --
   kicker flush with the portrait's top edge, closing element (the
   time-stat row) flush with its bottom edge, via the last-child rule
   below, instead of the whole block floating centered with room to
   spare above and below. */
/* Kicker-top/body-bottom pin retired -- see .pair-copy's comment above.
   Vertically centered now via .aunt-archive's align-items: center. */
.aunt-note {
  padding: clamp(24px, 4vw, 52px);
  background: var(--paper-batting);
}

.aunt-note h2 {
  font-size: var(--text-h2-compact);
  margin-bottom: clamp(30px, 3.6vw, 46px);
}

/* Same font-size and justified/left-last blocksatz treatment as
   .pair-copy p:not(.section-kicker) above -- these two sections are the
   same photo-plus-statement pattern and previously read at different type
   scales and alignments side by side. */
/* :not(.section-kicker), not :last-child -- the actual last child here is
   .time-stat (added when the old memory-section merged into this one),
   so the old :last-child selector stopped matching this paragraph
   entirely and it was silently falling back to the smaller default body
   size instead of the large size used everywhere else on the site. */
.aunt-note p:not(.section-kicker) {
  /* Matches .pair-copy's 620px cap -- this is the same photo-plus-copy
     idiom repeated across the site (.pair-copy, .aunt-note, .memory-lede,
     .transcript-intro-copy); previously only .pair-copy had an explicit
     cap while this one just filled its grid column unconstrained, which
     only stayed the same width by coincidence of the shared grid ratio
     and would have drifted apart on very wide viewports. */
  max-width: 620px;
  margin: 0;
  font-family: var(--body-font);
  font-size: var(--text-base);
  line-height: 1.55;
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

/* White card frame around the three panels so the strip lifts slightly
   off the page instead of sitting flush against the surrounding
   paper-batting background -- the 1px gap background still shows through
   between panels, now white instead of doubling as the page's own tone. */
.interview-strip {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  /* No outer padding -- the white card frame this used to have read as an
     unwanted margin around the section, when the three color panels
     should run edge-to-edge instead. The 1px gap (still showing
     var(--paper) beneath it) is only the thin seam between panels now,
     not a border around the whole strip. */
  gap: 1px;
  background: var(--paper);
  /* .transcript right below shares the same section-pad top padding that
     gives this strip its own generous space above (from .tempo-note's
     bottom padding), but its border-top sits flush at that boundary --
     a hard line right at the seam reads as noticeably tighter than the
     open padding above, even though the raw padding values match. Extra
     margin here closes that gap in perceived (not just measured)
     spacing. Now matched with an equal margin-top -- was relying purely
     on .tempo-note's own bottom padding for space above, which read as
     tighter than the (now deliberately larger) space below once that
     margin-bottom went in. Margin, not padding, since padding would add
     space INSIDE the colored panels themselves rather than around the
     strip as a whole. */
  margin-top: clamp(48px, 6vw, 96px);
  margin-bottom: clamp(88px, 11vw, 160px);
}

/* Was a muddy 2-stop gradient per quote, mixing blue/lila/yellow at low
   opacity into a color that did not read as any of the three -- flat
   tints now instead, each a single clean brand color at one consistent
   opacity, with a solid 3px top rule in the full-strength color so each
   quote is legible as "which color" at a glance rather than blending
   into a beige-grey smear. */
.interview-strip p {
  min-height: 180px;
  display: flex;
  align-items: end;
  margin: 0;
  padding: clamp(20px, 3vw, 36px);
  border-top: 3px solid transparent;
  background: var(--paper-batting);
  font-family: var(--body-font);
  font-size: var(--text-quote);
  font-style: italic;
  line-height: 1.35;
}

/* Opaque color-mixes now, not low-alpha rgba -- the container's own
   rgba(23,23,23,0.22) background (meant only as a thin divider line in
   the 1px grid gaps) was showing straight through each panel's
   translucent tint too, muddying blue/lila/hellblau down into a flat
   gray instead of three distinct pastel accents. */
.interview-strip p:nth-child(1) {
  border-top-color: var(--main-blue);
  /* main-blue is a much more saturated color than hellblau, so it needs a
     much lower mix percentage to land at the same paleness -- but 9% was
     too low to read as blue at all against the warm cream paper, which is
     what still left this panel looking gray instead of tinted. */
  background: color-mix(in srgb, var(--main-blue) 26%, var(--paper-batting));
}

.interview-strip p:nth-child(2) {
  border-top-color: var(--main-lila);
  background: color-mix(in srgb, var(--main-lila) 30%, var(--paper-batting));
}

.interview-strip p:nth-child(3) {
  border-top-color: var(--main-hellblau);
  background: color-mix(in srgb, var(--main-hellblau) 45%, var(--paper-batting));
}

.sensory {
  background: var(--paper-batting);
  align-items: stretch;
}

.sensory figure {
  grid-column: span 2;
  min-height: 0;
  aspect-ratio: 16 / 10;
}

.interview-video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--paper-batting);
}

.sensory-copy {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(24px, 4vw, 54px);
  background:
    linear-gradient(180deg, rgba(255, 253, 250, 0.6), rgba(239, 225, 207, 0.38)),
    var(--paper-batting);
}

.sensory-copy h2 {
  color: var(--main-blue);
  font-size: var(--text-h2-compact);
  margin-bottom: clamp(30px, 3.6vw, 46px);
}

.pulse-button,
.tool-actions button {
  min-height: 40px;
  border: 1px solid rgba(52, 79, 163, 0.48);
  background: transparent;
  color: inherit;
  padding: 9px 12px;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
}

.tool-actions button {
  border-color: rgba(128, 109, 176, 0.48);
}

.pulse-button:hover {
  background: var(--main-blue);
  color: var(--main-hellgelb);
}

.tool-actions button:hover,
.tool-actions button.primary {
  background: var(--main-hellblau);
  color: var(--main-lila);
}

.tool-actions button:disabled {
  cursor: default;
  opacity: 0.36;
}

/* --- Transcript: the full interview brought in from the book, laid out
   the way the book pages read it -- a narrow italic question column next
   to a wider plain-text answer column, grouped under numbered section
   titles, with occasional pull-quotes breaking the flow. --- */
.transcript {
  max-width: 1720px;
  margin: 0 auto;
  /* Deliberately plain white/paper here (was var(--paper-batting) cream,
     before that #fdf3d2 pale yellow, then a "warm batting" tan) -- this
     interview section is meant to stand out from the page's cream
     page-batting background as its own distinct zone, not blend into it.
     The border-rule convention below (same one used by .practice-quilt,
     .timeline-section, etc.) reinforces that same boundary. */
  background: var(--paper);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

/* Was a single centered text column -- now paired with a photo of Kim
   Minseon at work, so this opening reads as a portrait-plus-introduction
   like the other named sections on this page (.editorial-pair, the aunt
   archive) rather than the one text-only moment among photo pairs. */
/* Photo first, copy second (in the markup, not just visually) -- matches
   the photo-left/text-right order every other paired section on this
   page uses (.editorial-pair, .aunt-archive, .tempo-note). This one used
   to run copy-first/photo-second, the odd one out when scrolling past
   all four in sequence. Column split now mirrors .editorial-pair's own
   1.05fr/0.95fr for the same reason -- one consistent ratio, and a
   deliberately larger photo than the old 0.9fr/240px gave it. */
.transcript-intro {
  display: grid;
  grid-template-columns: minmax(320px, 1.05fr) minmax(280px, 0.95fr);
  gap: clamp(20px, 3vw, 48px);
  max-width: 1200px;
  margin: 0 auto clamp(48px, 7vw, 96px);
  align-items: center;
}

/* Kicker-top/body-bottom pin retired -- see .pair-copy's comment above.
   Vertically centered now via .transcript-intro's align-items: center. */
.transcript-intro-copy {
  /* Was 60ch (a character-based cap, unlike the px caps the rest of this
     family uses) -- switched to the same 620px used by .pair-copy,
     .aunt-note and .memory-lede so all four read as one consistent
     measure instead of a mix of ch/px caps that only landed close by
     coincidence. */
  max-width: 620px;
}

/* Back to square (was 3:2, briefly matching the source photo's own
   2000x1333 ratio to avoid cropping it) -- per the latest site-wide
   request, every paired photo goes square again, including this one. */
.transcript-intro figure {
  position: relative;
  overflow: hidden;
  aspect-ratio: 1 / 1;
}

.transcript-intro figure img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Shifted left -- the default center crop cut off too much of the
     hoop and hand on the left side once this went square. */
  object-position: 22% 50%;
}

.transcript-intro .section-kicker {
  color: var(--main-blue);
}

.transcript-intro h2 {
  max-width: 16ch;
  margin: 0 0 clamp(30px, 3.6vw, 46px);
}

.transcript-intro p:not(.section-kicker) {
  margin: 0;
  font-size: var(--text-base);
  line-height: 1.55;
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

/* The transcript below runs to nine numbered sections -- rather than a
   link that jumps a reader straight past all of that (the earlier
   approach), the body itself now opens collapsed to roughly one section's
   worth of reading, faded out at the bottom edge, with a button that
   expands it in place. Height is animated via JS (measuring scrollHeight)
   since transitioning to max-height:none can't itself be animated; this
   is a local content toggle, not a scroll-position animation, so it
   doesn't run into the sitewide no-smooth-scroll rule near the top of
   this file. */
/* Capped narrower than the 1720px .transcript container and centered --
   without this the Q&A grid's answer column (max-width 66ch) sat flush
   left inside a much wider 0.7fr track, leaving a large empty gap on the
   right and reading as if the whole interview hugged the left edge. */
.transcript-body {
  max-width: 1180px;
  margin: 0 auto;
  position: relative;
  overflow: hidden;
  transition: max-height 0.45s ease;
}

.transcript-body.is-collapsed {
  max-height: clamp(360px, 56vh, 620px);
}

/* Fades to var(--paper) again now that .transcript itself is back to a
   plain white/paper background -- fading to paper-batting (cream) here
   would leave a visible tinted patch at the bottom edge that doesn't
   match the section's own (white) background right behind it. */
.transcript-body.is-collapsed::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: clamp(90px, 14vw, 160px);
  background: linear-gradient(to bottom, rgba(255, 253, 250, 0), var(--paper) 82%);
  pointer-events: none;
}

.transcript-expand {
  display: flex;
  align-items: center;
  gap: 8px;
  width: fit-content;
  margin: 28px auto 0;
  padding: 12px 20px;
  border: 1px solid var(--main-blue);
  border-radius: 999px;
  background: none;
  font-family: var(--mono-font);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--main-blue);
  cursor: pointer;
  transition: background-color 0.2s ease, color 0.2s ease;
}

.transcript-expand:hover {
  background: var(--main-blue);
  color: var(--paper);
}

.transcript-expand-arrow {
  transition: transform 0.3s ease;
}

.transcript-expand[aria-expanded="true"] .transcript-expand-arrow {
  transform: rotate(180deg);
}

.transcript-section {
  padding: clamp(30px, 5vw, 56px) 0;
  border-top: 1px solid var(--line);
}

.transcript-section:first-of-type {
  border-top: 0;
  padding-top: 0;
}

.transcript-section-title {
  max-width: none;
  margin: 0 0 clamp(24px, 3.4vw, 42px);
  color: var(--main-blue);
  font-size: var(--text-h3);
  text-transform: none;
}

/* A small photo instead of the previous big full-format hero use of this
   same image -- placed right where the interview actually starts talking
   about her hands and the hoop, sized as a modest accent rather than a
   second hero image. */
.transcript-inset-photo {
  width: clamp(180px, 22vw, 300px);
  aspect-ratio: 1 / 1;
  margin: 0 0 clamp(24px, 3.4vw, 42px);
}

/* column-gap/row-gap split, not the shorthand -- a Q&A with more than one
   answer paragraph auto-flows each extra paragraph into its own grid row
   (see the comment below), and the shorthand's row-gap was adding on top
   of each paragraph's own margin-bottom, doubling the space between
   consecutive answers. row-gap: 0 leaves that spacing to the paragraphs
   themselves; column-gap keeps the question/answer split unchanged. */
.transcript-qa {
  display: grid;
  grid-template-columns: minmax(220px, 0.3fr) minmax(340px, 0.7fr);
  column-gap: clamp(24px, 3.6vw, 64px);
  row-gap: 0;
  margin-bottom: clamp(28px, 4vw, 52px);
}

.transcript-qa:last-child {
  margin-bottom: 0;
}

/* Pin question to the left rail and every answer paragraph to the right
   column explicitly -- without this, a Q&A with more than one answer
   paragraph would auto-flow its second paragraph back into column 1,
   stacking it underneath the question instead of continuing the answer. */
.transcript-qa .question {
  grid-column: 1;
  align-self: start;
  margin: 0;
  color: var(--main-blue);
  /* Matches .answer's own size (was var(--text-large), reading larger
     than the answer text right beside it) -- question and answer are the
     same running conversation, so they read at one consistent size. */
  font-size: var(--text-base);
  line-height: 1.4;
}

.transcript-qa .answer {
  grid-column: 2;
  margin: 0 0 1.1em;
  max-width: 66ch;
  font-size: var(--text-base);
  line-height: 1.6;
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

.transcript-qa .answer:last-of-type {
  margin-bottom: 0;
}

.transcript-pull {
  grid-column: 2;
  max-width: 46ch;
  margin: 6px 0 0;
  font-size: var(--text-quote);
  font-weight: 600;
  color: var(--main-blue);
  line-height: 1.35;
}

.transcript-close {
  margin-top: clamp(40px, 6vw, 80px);
  padding-top: clamp(30px, 5vw, 56px);
  border-top: 1px solid var(--line);
  display: grid;
  grid-template-columns: minmax(220px, 0.3fr) minmax(340px, 0.7fr);
  gap: clamp(24px, 3.6vw, 64px);
}

.transcript-close .transcript-section-title {
  margin: 0;
}

.transcript-close-body {
  display: grid;
  grid-template-columns: repeat(2, minmax(220px, 1fr));
  gap: clamp(20px, 3vw, 42px);
  /* Same bug class as .transcript-qa: the 3rd paragraph wraps to row 2,
     and without this, default align-items:stretch makes the shorter
     paragraph in row 1 stretch to match its taller row-mate, leaving
     invisible space below its own text before the row-gap even starts --
     reads as a doubled gap above the 3rd paragraph. Top-aligning each
     paragraph in its own cell removes that phantom space. */
  align-items: start;
}

.transcript-close-body p {
  margin: 0 0 1.1em;
  font-size: var(--text-base);
  line-height: 1.6;
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

.transcript-close-media {
  grid-column: 1 / -1;
  margin: clamp(24px, 3.6vw, 48px) 0 0;
  aspect-ratio: 16 / 9;
}

.transcript-close-media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.transcript-close-body p:last-child {
  margin-bottom: 0;
}

/* Photo / video breaks inside the transcript: real documentary media from
   the interview, placed at intervals to give the long reading a visual
   rhythm. Shares the transcript-qa grid (label column + media column) so
   it lines up with the surrounding question/answer rows. */
.transcript-media {
  display: grid;
  grid-template-columns: minmax(220px, 0.3fr) minmax(340px, 0.7fr);
  gap: clamp(24px, 3.6vw, 64px);
  margin: clamp(4px, 1vw, 14px) 0 clamp(40px, 5.5vw, 68px);
}

.transcript-media .break-label {
  margin: 0;
  align-self: start;
  color: var(--main-blue);
  font-family: var(--mono-font);
  font-style: italic;
  font-size: var(--text-base);
}

.transcript-media figure {
  margin: 0;
}

.transcript-media img,
.transcript-media video {
  display: block;
  width: 100%;
  height: auto;
  max-height: 78vh;
  object-fit: cover;
}

.transcript-media figcaption {
  margin-top: 12px;
  color: var(--ink);
  font-size: 14px;
}

/* tempo-note: same photo-plus-text idiom used by editorial-pair and
   aunt-archive elsewhere on this page (image sits beside the copy,
   square-cropped to match the site's print-grid identity). */
.tempo-note {
  display: grid;
  /* Widened from 0.85fr/1.15fr to match .editorial-pair's ratio -- gives
     the square photo more width (so more height) relative to the text
     column, closing most of the alignment gap before JS even has to
     stretch the copy column's min-height very far. */
  grid-template-columns: minmax(320px, 1.05fr) minmax(320px, 0.95fr);
  /* Was clamp(20px, 3vw, 48px), then clamp(28px, 4vw, 64px) -- widened
     again to match .practice-quilt's own generous photo-to-text
     breathing room, paired with the smaller edge padding below (shifting
     space from the outer margin into the middle gap, not adding net
     width to the section). */
  gap: clamp(40px, 6vw, 96px);
  /* .section-pad's own max(24px, 6vw) horizontal padding read as more
     edge margin than .practice-quilt sits at -- narrower edge padding
     here, with the extra space above moved into the gap instead. */
  padding-inline: max(16px, 3vw);
  /* Vertically centers kicker+heading+body as one block beside the
     photo -- replaces the old kicker-top/body-bottom pin (and the JS
     min-height measuring that came with it, now removed from
     site-effects.js). */
  align-items: center;
  /* Matches .editorial-pair/.transcript's 1720px cap -- same fix, same
     reason: on wide monitors this grid ran full-width, growing the photo
     and leaving the gap/edge padding looking disproportionate (gap reads
     too tight, unused space past the text reads as too much edge margin)
     even though neither value itself had changed. */
  max-width: 1720px;
  margin-left: auto;
  margin-right: auto;
}

/* Plain square crop, no mat -- the previous photo needed the white inset
   border to keep its busy striped background from crowding the frame;
   this one is already shot on a clean white studio backdrop, so a full-
   bleed square crop reads cleanly on its own without an artificial
   border faking the same effect. */
.tempo-note-photo {
  position: relative;
  overflow: hidden;
  aspect-ratio: 1 / 1;
}

.tempo-note-photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Same photo-plus-text idiom as .tempo-note just above -- video stands in
   for the photo, same column split (widened the same way, from
   0.85fr/1.15fr to 1.05fr/0.95fr -- the 16:9 video is the shortest figure
   on the site, so it needs every bit of extra width/height it can get to
   keep pace with even a shrunk text column). */
.social-note {
  display: grid;
  grid-template-columns: minmax(320px, 1.05fr) minmax(320px, 0.95fr);
  /* Widened from clamp(20px, 3vw, 48px), then clamp(28px, 4vw, 64px) --
     matched to .tempo-note's own widened gap, same reasoning: shifted
     from the outer edge padding (below) into the middle gap instead. */
  gap: clamp(40px, 6vw, 96px);
  /* Same edge-padding reduction as .tempo-note -- see its comment. */
  padding-inline: max(16px, 3vw);
  /* Was align-items: center, which let the video sit at its own
     aspect-ratio-derived size regardless of the row's actual height.
     Default stretch (same as .workshop-invite, no override there either)
     lets the video column match the row's full height instead, so the
     video reads at the same scale/prominence as .workshop-invite's --
     see .social-note-video below for the matching size mechanism. */
  /* Same 1720px cap as .tempo-note/.editorial-pair/.transcript -- keeps
     this grid from stretching edge-to-edge on wide monitors. */
  max-width: 1720px;
  margin-left: auto;
  margin-right: auto;
}

/* Was a fixed aspect-ratio: 16/9 (own-width-derived size, independent of
   the row's actual height) -- switched to the same mechanism
   .workshop-invite-video uses instead: width/height 100% of the now
   row-stretched figure, capped by a viewport-relative max-height. Reads
   noticeably larger/more prominent now, matching .workshop-invite's
   video scale instead of sitting smaller next to it. */
.social-note-video {
  width: 100%;
  height: 100%;
  max-height: 74svh;
  overflow: hidden;
}

.social-note-video video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Kicker-top/body-bottom pin retired -- see .pair-copy's comment above.
   Vertically centered now via .tempo-note/.social-note's
   align-items: center. Scoped per-section (not applied to .memory-lede
   globally) since this class is shared with a couple of other layouts. */

.question {
  margin-top: 0;
  font-size: var(--text-large);
  font-style: italic;
}

.answer {
  margin-bottom: 0;
}

/* Was a solid var(--main-blue) fill -- now folds in what used to be the
   separate .next-layer section right after it (same closing statement +
   CTA, just split across two back-to-back sections for no real reason).
   Plain page background instead of a color block, since the section is
   doing double duty now (closing quote + transition CTA) rather than
   reading as one bold isolated beat. */
.warm-gallery {
  background: var(--paper-batting);
  color: var(--ink);
}

/* Large, near-fullscreen treatment for the one remaining photo -- it's
   carrying the whole section visually now instead of sharing space with
   two others. */
.warm-gallery figure {
  aspect-ratio: 1 / 1;
  max-height: clamp(420px, 56vw, 760px);
}

.warm-gallery-quote {
  padding: clamp(16px, 2vw, 32px);
}

/* Blue (was hellgelb here, lila on the folded-in .next-layer kicker) --
   one consistent accent color for the whole combined section now,
   matching the rest of this page's blue-batting identity instead of
   mixing three different accent colors across what is now one block. */
.warm-gallery-quote .section-kicker {
  color: var(--main-blue);
}

/* Demoted from the large bold pull-quote (--text-quote/600) it used to
   share with .transcript-pull -- at that size/weight it competed with the
   h2 right below it for the reader's attention instead of leading into
   it. Now sits at plain body-copy size/weight, matching the standard
   kicker (small) -> h2 (big statement) -> body (--text-base) hierarchy
   used everywhere else on the site. Font-family/style are also pulled
   out of the mono/italic caption voice here and forced back to the
   regular body font -- the base `figcaption, blockquote` rule sets
   mono+italic with !important, so this needs an equally-specific
   !important override to read as plain body text instead of a caption. */
.warm-gallery-quote blockquote {
  margin: 0 0 20px;
  /* Matches the site's established ~46ch body-copy measure now that this
     reads as body text rather than a pull-quote (see the size/weight
     change above) -- was still 34ch from its old pull-quote sizing. */
  max-width: 46ch;
  font-family: var(--body-font) !important;
  font-style: normal;
  font-size: var(--text-base);
  font-weight: 400;
  line-height: 1.55;
  color: var(--ink);
}

/* Was max-width: 11ch (folded in from the old .next-layer h2) -- at this
   heading's actual size that wrapped "The backing is where your hand
   enters." into four choppy, uneven lines instead of reading as one
   confident statement the way headings do elsewhere (see .memory-lede
   h2, e.g. .social-note's "A handmade quilt is never a neutral gift.").
   Wider cap lets it fall into two natural lines instead. */
.warm-gallery-quote h2 {
  max-width: 22ch;
  margin-bottom: 24px;
}

/* Before/after record of the community quilt: the loose, unsashed block
   grid on one side, the piece Heike and Franziska actually finished
   (sashed, bordered, quilted) on the other, plus a small strip of
   detail shots underneath for texture. Sits between the workshop photo
   carousel and the digital tool, closing the loop the workshop-trace
   copy opens ("...before every square was joined into one community
   quilt"). */
.community-result {
  padding: clamp(48px, 7vw, 96px) max(24px, 6vw);
  background: var(--paper);
  border-top: 1px solid var(--line);
}

/* Text and image side by side rather than stacked -- the slider reads
   as the section's visual anchor next to its own copy instead of a
   full-width block underneath it. */
.community-result-grid {
  display: grid;
  grid-template-columns: minmax(300px, 0.85fr) minmax(340px, 1fr);
  gap: clamp(28px, 4vw, 64px);
  align-items: center;
}

.community-result-head h2 {
  margin: 6px 0 0;
}

.community-result-head p:not(.section-kicker) {
  margin: 14px 0 0;
  font-family: var(--body-font);
  font-size: var(--text-base);
  line-height: 1.55;
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

.quilt-compare-frame {
  position: relative;
  aspect-ratio: 1;
  /* Square, but capped so the whole thing fits on screen at once --
     without this, a wide desktop content column (well over 1000px)
     would make an aspect-ratio:1 square taller than the viewport
     itself, forcing a scroll within the image. */
  width: min(100%, 74vh);
  margin: 0 auto;
  overflow: hidden;
  background: var(--paper-top);
  cursor: ew-resize;
  touch-action: none;
  --pos: 50%;
}

.quilt-compare-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.quilt-compare-before {
  z-index: 1;
}

/* The after-image sits in its own clipped wrapper rather than being
   clipped directly, so its own inset:0 image stays pinned to the full
   frame -- narrowing the clip-path reveals more of a fixed image
   instead of stretching/squashing a resized one. Clipped from the left
   (not the right) so "after" only ever shows to the right of the
   handle -- before reads left, after reads right, matching how you'd
   naturally scan the image left to right. */
.quilt-compare-after-clip {
  position: absolute;
  inset: 0;
  z-index: 2;
  overflow: hidden;
  clip-path: inset(0 0 0 var(--pos));
}

.quilt-compare-handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--pos);
  z-index: 4;
  display: flex;
  align-items: center;
  justify-content: center;
  transform: translateX(-50%);
  outline: none;
}

.quilt-compare-handle::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 2px;
  background: rgba(255, 253, 250, 0.9);
  transform: translateX(-50%);
  pointer-events: none;
}

.quilt-compare-handle-grip {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: var(--paper);
  border: 1px solid var(--line);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink);
  box-shadow: 0 2px 10px rgba(23, 23, 23, 0.16);
}

.quilt-compare-handle:focus .quilt-compare-handle-grip {
  border-color: var(--main-lila);
  box-shadow: 0 0 0 3px rgba(128, 109, 176, 0.28);
}


/* This is the site's interactive highlight, not just another editorial
   section -- it keeps the bold full-color treatment of a page hero (see
   .backing-hero) instead of blending into the plain-paper sections
   around it, so arriving here still reads as arriving somewhere. But its
   padding is now a compact banner, not a near-fullscreen moment: this
   band sits directly above the tool card below, and the two need to read
   as one connected unit on a normal laptop screen rather than the CTA
   eating the whole viewport by itself before the actual tool even comes
   into view. */
/* Two columns instead of one stacked-left block: kicker+heading centered
   in the left half, body copy centered in the right half, both riding the
   same vertical center so the gap between them reads as one even space
   rather than a wide dead zone next to a left-pinned block. */
.tool-cta {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  /* Was center -- the body paragraph is shorter than the kicker+h2
     column, so centering left it floating in the middle of that taller
     block instead of reading off the same baseline as the heading's own
     last line. end lines the paragraph's bottom up with the heading
     column's bottom (the last line of "make your own square."). */
  align-items: end;
  gap: clamp(24px, 5vw, 64px);
  padding: clamp(24px, 3vw, 44px) max(24px, 6vw);
  background: var(--main-hellblau);
  border-top: 1px solid var(--line);
  /* Same reasoning as .quilt-tool's scroll-margin-top below -- the
     Playground button lands here first (see playground-jump.js), and
     without this the sticky nav would sit on top of this band's own
     top edge. */
  scroll-margin-top: 58px;
}

.tool-cta-heading {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* Same hellblau-ground/lila-text pairing as .backing-hero just above this
   section on the page (see .page-backing .hero-copy h1) -- one consistent
   color combination for the page's two bold color moments instead of the
   hero going lila-on-hellblau and this section quietly switching to
   plain ink. */
.tool-cta .section-kicker {
  color: var(--main-lila);
}

.tool-cta h2 {
  margin: 0;
  max-width: 26ch;
  font-size: var(--text-h2-compact);
}

.tool-cta p:not(.section-kicker) {
  max-width: 56ch;
  margin: 0;
  font-family: var(--body-font);
  font-size: var(--text-base);
  color: var(--main-lila);
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

.quilt-tool {
  --workshop-height: calc(100svh - 116px);
  min-height: var(--workshop-height);
  /* The sticky site-nav (58px) would otherwise sit on top of this
     section's own top edge when the Playground button scrolls here --
     this offsets the scroll landing point so the tool never lands
     partly hidden behind the nav bar. */
  scroll-margin-top: 78px;
  display: grid;
  /* Wider panel column (was 320-380px, then 360-480px) so the template
     grid can run 4 across instead of 3, and so the Shape/Color pair (see
     .tool-group-pair) has enough room that Color's own swatches -- each
     labeled with a full color name like "Hellgelb" -- don't get squeezed
     against the panel's own edge. */
  grid-template-columns: minmax(0, 1fr) minmax(360px, 560px);
  gap: 12px;
  padding: 12px;
  background:
    radial-gradient(circle at 22px 34px, rgba(101, 91, 70, 0.12) 0 0.8px, transparent 1.4px),
    radial-gradient(circle at 92px 78px, rgba(101, 91, 70, 0.08) 0 0.9px, transparent 1.5px),
    linear-gradient(rgba(236, 232, 221, 0.94), rgba(236, 232, 221, 0.94)),
    var(--paper-backing);
  background-size: 130px 130px, 170px 170px, auto, auto;
  color: var(--ink);
  align-items: stretch;
}

.tool-panel {
  display: flex;
  flex-direction: column;
  gap: 9px;
  padding: 18px 14px 16px 26px;
  /* min-height (not a capped height + internal scroll) -- the panel's own
     content now dictates how tall this whole section gets, and the canvas
     column stretches to match (see .cloth-stage-wrap's min-height and
     .quilt-tool's align-items: stretch), instead of the panel scrolling
     inside a box pinned to one screen's height. Trades "always exactly one
     screen tall" for "never needs its own separate scrollbar" -- on a short
     viewport the whole section (and page) just runs a bit taller instead of
     hiding the Community Quilt block behind a scroll. */
  min-height: calc(var(--workshop-height) - 24px);
  background:
    radial-gradient(circle at 18px 26px, rgba(101, 91, 70, 0.1) 0 0.8px, transparent 1.4px),
    linear-gradient(rgba(255, 253, 250, 0.5), rgba(255, 253, 250, 0.5)),
    var(--paper-backing);
  background-size: 120px 120px, auto, auto;
  color: var(--ink);
  box-shadow: inset 0 0 0 1px rgba(23, 23, 23, 0.24);
  border-top: 8px solid var(--main-hellblau);
}

.tool-panel h2 {
  margin: 2px 0 0;
  font-size: var(--text-h2-compact);
  line-height: 0.95;
}

.tool-panel .section-kicker {
  color: var(--ink);
}

.tool-group {
  display: grid;
  gap: 6px;
}

.tool-label {
  margin: 0;
  color: var(--ink);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.patch-palette {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 8px;
}

/* Shape and Color used to be two full-width stacked tool-groups, each with
   its own label sitting above a fairly short, wide grid -- most of that
   width went unused (2-4 icon-sized swatches don't need the panel's full
   380px), and stacking them cost a whole extra block of vertical room the
   panel could not really spare. Placing them side by side buys back that
   height without shrinking either palette's actual button count or
   making the icons cramped. */
.tool-group-pair {
  display: flex;
  gap: 14px;
  align-items: start;
}

.tool-group-pair .tool-group {
  min-width: 0;
}

.tool-group-pair .tool-group:first-child {
  flex: 1.3 1 0%;
}

.tool-group-pair .tool-group:last-child {
  flex: 1 1 0%;
}

.form-palette {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 6px;
}

.template-palette {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 5px;
  max-height: none;
  overflow: visible;
  padding-right: 0;
}

.color-palette {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 6px;
}

.shape-swatch,
.template-swatch,
.fabric-swatch,
.color-swatch,
.size-palette button,
.segmented button,
.tool-actions button {
  border: 1px solid rgba(23, 23, 23, 0.24);
  background: rgba(255, 248, 239, 0.62);
  cursor: pointer;
  border-radius: 0;
  color: var(--ink);
}

.shape-swatch.active,
.template-swatch.active,
.fabric-swatch.active,
.color-swatch.active,
.size-palette button.active,
.segmented button.active {
  outline: 2px solid var(--main-lila);
  outline-offset: 2px;
}

.shape-swatch {
  min-height: 38px;
  display: grid;
  place-items: center;
  padding: 5px;
}

.shape-icon {
  display: block;
  /* --shape-scale (set per form in JS, based on its actual grid footprint)
     shrinks smaller shapes' icons so e.g. the 1x1 square reads visibly
     smaller than the 2x2 "Large square" -- --shape-ratio alone can't do
     that since both are the same 1:1 ratio. */
  width: calc(min(76%, 46px) * var(--shape-scale, 1));
  aspect-ratio: var(--shape-ratio, 1 / 1);
  background: var(--main-blue);
  clip-path: var(--piece-clip);
  box-shadow: inset 0 0 0 1px rgba(23, 23, 23, 0.16);
}

.template-swatch {
  aspect-ratio: 1;
  position: relative;
  overflow: hidden;
  background: rgba(255, 248, 239, 0.78);
}

.template-swatch::before {
  content: none;
}

.template-swatch::after {
  content: attr(data-label);
  position: absolute;
  left: 7px;
  right: 7px;
  bottom: 6px;
  font-size: 7.5px;
  font-weight: 820;
  letter-spacing: 0.04em;
  line-height: 1.05;
  color: var(--ink);
  text-align: left;
  text-transform: uppercase;
}

.template-mini {
  position: absolute;
  inset: 8px 8px 25px;
  width: calc(100% - 16px);
  height: calc(100% - 33px);
}

.template-mini path,
.template-mini ellipse {
  fill: rgba(52, 79, 163, 0.26);
  stroke: rgba(52, 79, 163, 0.25);
  stroke-width: 0.18;
  vector-effect: non-scaling-stroke;
}

.template-mini .mini-grid {
  fill: none;
  stroke: rgba(23, 23, 23, 0.13);
  stroke-width: 0.12;
}

/* :not(.mini-grid) is redundant for <ellipse> (grid lines are always
   <path>, never <ellipse>) but harmless to include, so both element
   types can share one selector list here. */
.template-swatch[data-tone="blank"] .template-mini path:not(.mini-grid),
.template-swatch[data-tone="blank"] .template-mini ellipse {
  fill: none;
  stroke: none;
}

.template-swatch[data-tone="lilac"] .template-mini path:not(.mini-grid),
.template-swatch[data-tone="lilac"] .template-mini ellipse {
  fill: rgba(128, 109, 176, 0.3);
  stroke: rgba(128, 109, 176, 0.25);
}

.template-swatch[data-tone="light-blue"] .template-mini path:not(.mini-grid),
.template-swatch[data-tone="light-blue"] .template-mini ellipse {
  fill: rgba(185, 228, 243, 0.55);
  stroke: rgba(52, 79, 163, 0.2);
}

.template-swatch[data-tone="yellow"] .template-mini path:not(.mini-grid),
.template-swatch[data-tone="yellow"] .template-mini ellipse {
  fill: rgba(255, 241, 168, 0.72);
  stroke: rgba(52, 79, 163, 0.2);
}

/* Log Cabin's own per-piece tones, mirrored into the small swatch button so
   it matches the live on-canvas guide instead of falling back to one flat
   tone-wide fill. Placed after the tone rule above so it wins the tie on
   source order (both selectors resolve to the same specificity). */
.template-swatch[data-tone] .template-mini path.log-square-a,
.template-swatch[data-tone] .template-mini path.log-bar-a {
  fill: rgba(255, 241, 168, 0.72);
  stroke: rgba(52, 79, 163, 0.2);
}

.template-swatch[data-tone] .template-mini path.log-square-b,
.template-swatch[data-tone] .template-mini path.log-bar-b {
  fill: rgba(185, 228, 243, 0.72);
  stroke: rgba(52, 79, 163, 0.2);
}

.template-swatch[data-tone] .template-mini path.log-square-c,
.template-swatch[data-tone] .template-mini path.log-bar-c {
  fill: rgba(128, 109, 176, 0.62);
  stroke: rgba(52, 79, 163, 0.2);
}

.template-swatch[data-tone] .template-mini path.log-square-d,
.template-swatch[data-tone] .template-mini path.log-bar-d {
  fill: rgba(52, 79, 163, 0.55);
  stroke: rgba(52, 79, 163, 0.3);
}

.template-swatch[data-tone] .template-mini path.log-center {
  fill: rgba(255, 253, 250, 0.95);
  stroke: rgba(52, 79, 163, 0.3);
}

.color-swatch {
  min-height: 30px;
  display: grid;
  grid-template-columns: 22px 1fr;
  align-items: center;
  gap: 8px;
  padding: 4px 6px;
  background: rgba(255, 248, 239, 0.52);
  font-size: 11px;
  font-weight: 820;
  letter-spacing: 0.04em;
  text-align: left;
  text-transform: uppercase;
}

.color-swatch::before {
  content: "";
  width: 18px;
  aspect-ratio: 1;
  background: var(--swatch-color);
  border: 1px solid rgba(23, 23, 23, 0.42);
}

.color-swatch::after {
  content: attr(data-label);
}

.color-swatch.active {
  box-shadow: inset 0 0 0 2px var(--paper);
}

.segmented {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 4px;
}

.mode-palette {
  grid-template-columns: repeat(3, 1fr);
}

.segmented button {
  min-height: 30px;
  padding: 0 6px;
  font-size: 10px;
  font-weight: 820;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.size-palette {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.size-palette button {
  min-height: 42px;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.palette-swatch {
  aspect-ratio: 1;
  border: 1px solid rgba(23, 23, 23, 0.22);
  background: var(--patch-bg, var(--paper));
  cursor: grab;
  overflow: hidden;
}

.palette-swatch:active {
  cursor: grabbing;
}

.palette-swatch::before,
.patch-visual {
  width: 100%;
  height: 100%;
  background: var(--patch-bg, var(--paper));
  overflow: hidden;
}

.palette-swatch::before {
  content: "";
  display: block;
}

.palette-swatch::before,
.patch-visual::before {
  content: "";
  display: block;
  width: 100%;
  height: 100%;
  background: var(--patch-fg, var(--blue-dark));
  -webkit-mask: var(--patch-mask) center / 78% 78% no-repeat;
  mask: var(--patch-mask) center / 78% 78% no-repeat;
}

.tool-actions {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 5px;
  margin-top: auto;
  padding-top: 8px;
  border-top: 1px solid rgba(23, 23, 23, 0.18);
}

.tool-actions button.primary {
  grid-column: 1 / -1;
}

.tool-actions button {
  min-height: 28px;
  padding: 0 8px;
  font-size: 10px;
  font-weight: 860;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

.tool-actions button.primary {
  background: var(--main-hellblau);
  color: var(--ink);
}

/* Share only ever gets un-hidden by JS after feature-detecting
   navigator.share -- most desktop/kiosk browsers never see this button at
   all, only phones/tablets that can actually act on a tap. */
.tool-actions button.is-hidden {
  display: none;
}

.tool-note {
  /* No max-width cap -- this used to wrap at 38ch, leaving a noticeably
     empty right-hand strip inside the panel even though the panel itself
     is much wider; letting it run the panel's own full width (the
     panel's own padding is what keeps it off the edge) uses the space
     instead of wasting it. */
  margin: 8px 0 0;
  line-height: 1.45;
  font-size: 10px;
  font-weight: 760;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
}

/* --- Community quilt: submit block inside the tool panel, and the live
   gallery section further down the page. Visitors add their finished
   square to a shared Supabase-backed gallery -- see quilt-tool.js for the
   upload/fetch logic. Open only for the exhibition window (4-24 Aug); the
   note text and button both react to that same cutoff in JS rather than
   this CSS needing its own date logic. */
.community-submit {
  margin-top: 4px;
  padding-top: 10px;
  border-top: 1px solid rgba(23, 23, 23, 0.18);
}

/* Name field and submit button share one row now (was two full-width
   rows stacked) -- a small, deliberate density gain in a panel that
   otherwise scrolls internally, so the most-used bottom of the panel
   (the actual submit action) sits closer to the palettes above it. */
.community-submit-row {
  display: flex;
  gap: 6px;
}

.community-name-input {
  flex: 1;
  min-width: 0;
  padding: 9px 10px;
  border: 1px solid rgba(23, 23, 23, 0.24);
  background: rgba(255, 248, 239, 0.62);
  font-family: var(--body-font);
  font-size: 13px;
  color: var(--ink);
}

.community-name-input::placeholder {
  color: rgba(23, 23, 23, 0.45);
}

.community-name-input:disabled {
  opacity: 0.5;
}

/* Same visual language as .tool-actions button.primary (hellblau fill,
   uppercase mono label) but this button lives outside the .tool-actions
   grid, so it needs its own copy of that styling rather than relying on
   the .tool-actions-scoped selector. */
.community-submit-button {
  flex: none;
  min-height: 34px;
  padding: 0 12px;
  border: 1px solid rgba(23, 23, 23, 0.24);
  border-radius: 0;
  background: var(--main-hellblau);
  color: var(--ink);
  font-size: 10px;
  font-weight: 860;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  cursor: pointer;
}

.community-submit-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.community-status {
  margin: 8px 0 0;
  min-height: 13px;
  font-size: 10px;
  font-weight: 760;
  letter-spacing: 0.06em;
  color: var(--ink);
}

.community-status.is-error {
  color: #a33b3b;
}

/* Brief "Undo" link after a successful submission -- a small text button,
   not styled like the primary Add-to-quilt action, since it is a rare
   correction path rather than a main control. Hidden by default/after its
   timeout via .is-hidden. */
.community-undo {
  display: inline-block;
  margin-top: 4px;
  padding: 0;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: 10px;
  font-weight: 760;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--main-lila);
  text-decoration: underline;
  cursor: pointer;
}

.community-undo.is-hidden {
  display: none;
}

/* "Scan to save" overlay -- a small fixed-position modal rather than a
   permanent block in the panel, since the panel is already tuned to fit
   one screen (see .tool-panel/.tool-group-pair) and a QR code doesn't
   need to sit there all the time, only on demand. */
.qr-modal {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: grid;
  place-items: center;
  padding: 24px;
  background: rgba(23, 23, 23, 0.5);
}

.qr-modal.is-hidden {
  display: none;
}

.qr-modal-card {
  position: relative;
  width: min(340px, 100%);
  padding: 28px 24px 24px;
  background: var(--paper);
  box-shadow: inset 0 0 0 1px rgba(23, 23, 23, 0.24), 0 20px 48px rgba(23, 23, 23, 0.3);
  text-align: center;
}

.qr-modal-card h3 {
  margin: 4px 0 0;
  color: var(--main-lila);
  font-size: var(--text-h3);
  line-height: 1.05;
}

.qr-modal-note {
  margin: 8px 0 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--ink);
}

.qr-modal-code {
  display: grid;
  place-items: center;
  margin: 18px 0 0;
}

.qr-modal-code svg {
  width: 200px;
  height: 200px;
}

.qr-modal-link {
  display: block;
  margin-top: 12px;
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--main-lila);
  word-break: break-all;
}

.qr-modal-close {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 28px;
  height: 28px;
  border: 1px solid rgba(23, 23, 23, 0.24);
  background: var(--paper);
  color: var(--ink);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}

.qr-modal-close:hover {
  background: var(--main-hellblau);
}

/* Gets the same paper-backing texture + colored top border as the
   canvas/panel cards right above it (see .cloth-stage-wrap, .tool-panel)
   instead of quietly fading back to plain white -- this is the payoff
   of the whole Playground ("your square joins here"), so it reads as a
   destination in its own right, not an afterthought below the fold. */
.community-gallery {
  background:
    radial-gradient(circle at 22px 34px, rgba(101, 91, 70, 0.12) 0 0.8px, transparent 1.4px),
    radial-gradient(circle at 92px 78px, rgba(101, 91, 70, 0.08) 0 0.9px, transparent 1.5px),
    linear-gradient(rgba(236, 232, 221, 0.94), rgba(236, 232, 221, 0.94)),
    var(--paper-backing);
  background-size: 130px 130px, 170px 170px, auto, auto;
  border-top: 8px solid var(--main-lila);
}

.community-gallery-head {
  max-width: 64ch;
  margin: 0 0 clamp(28px, 4vw, 48px);
}

.community-gallery-head h2 {
  margin: 6px 0 0;
  font-size: var(--text-h2-compact);
  line-height: 1.02;
}

.community-count {
  display: inline-block;
  margin: 16px 0 0;
  padding: 4px 12px;
  background: var(--main-lila);
  color: var(--paper);
  font-family: var(--mono-font);
  font-size: 13px;
  letter-spacing: 0.02em;
}

.community-gallery-head p:last-child {
  margin: 16px 0 0;
  font-family: var(--body-font);
  /* Same fix as .timeline-intro p:last-child above -- was missing and
     fell back to the smaller global default instead of the site's
     canonical body-paragraph size (now --text-base, matching the
     interview transcript). */
  font-size: var(--text-base);
  text-align: justify;
  text-align-last: left;
  hyphens: none;
}

/* Base fallback before JS has run (or if it never does): auto-fill +
   minmax still grows sensibly from one square up to however many visitors
   have submitted. Once quilt-tool.js's renderCommunityGallery() runs, it
   sets an explicit column count (~sqrt of the item count) as an inline
   style here, so the block as a whole reads as one assembled square --
   like the individual workshop blocks becoming one shared quilt -- instead
   of a strip that only wraps because the container ran out of width.
   Gap is intentionally tight (tighter than the 10px this used to be) so
   the squares read as sashed-together patches, not a spaced-out grid.
   Columns are capped at 200px (both here and in the inline style JS sets)
   so a handful of squares never get stretched to fill the whole section
   width -- with only one or two submitted, that used to blow a single
   square up to 700px+, well past its 800px source render's comfortable
   display size, and the upscaling read as visibly soft/pixelated.
   justify-content centers the (now width-capped) grid instead of letting
   the last column stretch to fill the remaining row width. */
.community-gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 200px));
  justify-content: center;
  gap: 3px;
}

.community-gallery-grid figure {
  margin: 0;
  aspect-ratio: 1;
  overflow: hidden;
  padding: 5px;
  background: var(--paper);
  /* Assembling-in state -- each square starts small, rotated and
     invisible, then snaps into its grid cell (see .is-placed below).
     quilt-tool.js staggers each figure's transition-delay by its index
     so the whole grid reads as pieces joining into one surface, echoing
     the workshop photos above of individual blocks becoming one quilt,
     rather than everything just popping in at once. */
  opacity: 0;
  transform: scale(0.4) rotate(-9deg);
  transition: opacity 0.5s ease, transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}

.community-gallery-grid figure.is-placed {
  opacity: 1;
  transform: scale(1) rotate(0deg);
}

@media (prefers-reduced-motion: reduce) {
  .community-gallery-grid figure {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

.community-gallery-grid img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A proper placeholder card (dashed border, generous height) instead of
   one thin italic line -- with no squares submitted yet, that line was
   the section's entire visible content beneath the heading, which read
   as broken/unfinished rather than simply "early." */
.community-gallery-empty {
  display: grid;
  place-content: center;
  gap: 4px;
  min-height: 220px;
  margin: 0;
  padding: 24px;
  border: 1.5px dashed rgba(146, 121, 173, 0.45);
  font-family: var(--mono-font);
  color: rgba(23, 23, 23, 0.55);
  text-align: center;
}

.community-gallery-empty p {
  margin: 0;
}

.community-gallery-empty p:first-child {
  font-style: italic;
}

.community-gallery-empty p:last-child {
  color: var(--main-lila);
}

.community-gallery-empty.is-hidden {
  display: none;
}

.cloth-stage-wrap {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: 10px;
  min-width: 0;
  /* min-height (not height) so this column can grow past one screen to
     match however tall the panel's own content turns out to be (see
     .tool-panel) -- it still won't ever end up shorter than that. */
  min-height: calc(var(--workshop-height) - 24px);
  align-self: stretch;
  /* Same "card" treatment as .tool-panel right beside it (paper-backing
     texture, inset hairline, colored top border) -- previously this
     column was just the bare section background, so the canvas read as
     a small square adrift in a lot of plain, undesigned space instead of
     the two columns forming one deliberate, matched pair. */
  padding: 18px;
  background:
    radial-gradient(circle at 18px 26px, rgba(101, 91, 70, 0.1) 0 0.8px, transparent 1.4px),
    linear-gradient(rgba(255, 253, 250, 0.5), rgba(255, 253, 250, 0.5)),
    var(--paper-backing);
  background-size: 120px 120px, auto, auto;
  box-shadow: inset 0 0 0 1px rgba(23, 23, 23, 0.24);
  border-top: 8px solid var(--main-hellblau);
}

/* Sizing frame around the actual canvas: a container-query context so
   the square below can measure ITS available box (not the wrap's,
   which also includes the meta bar) and pick the largest size that
   fits both dimensions, then center itself in whatever's left over.
   Previously the square only read its own height and let width follow
   from that, ignoring how much horizontal room the wide left-hand
   column actually had -- on any screen wider than it is tall, that
   left most of the column empty next to a small, corner-pinned grid. */
.cloth-stage-frame {
  container-type: size;
  min-width: 0;
  min-height: 0;
  display: grid;
  place-items: center;
}

.cloth-meta {
  display: flex;
  justify-content: space-between;
  gap: 18px;
  color: var(--main-lila);
}

.cloth-stage {
  position: relative;
  /* Contains every placed piece's own z-index (which climbs indefinitely
     as pieces get added/moved/rotated -- see state.zIndex) inside this
     element's own stacking context, so a well-used canvas can never end
     up outranking page-level fixed/sticky elements like the site nav or
     the QR modal. Without this, a piece's z-index was compared globally
     and, after enough interactions, could climb past the nav's (50) or
     even the modal's (80) and render on top of both. */
  isolation: isolate;
  min-height: min(620px, calc(100svh - 138px));
  overflow: hidden;
  background:
    linear-gradient(rgba(23, 23, 23, 0.04) 1px, transparent 1px),
    linear-gradient(90deg, rgba(23, 23, 23, 0.04) 1px, transparent 1px),
    radial-gradient(circle at 50% 20%, rgba(255, 255, 255, 0.42), transparent 38%),
    var(--paper);
  background-size: 56px 56px, 56px 56px, auto, auto;
  border: 1px solid rgba(255, 248, 239, 0.22);
  outline: none;
}

.cloth-stage.grid-cloth {
  min-height: 0;
  aspect-ratio: 1;
  width: min(100cqw, 100cqh);
  height: min(100cqw, 100cqh);
  /* Capped so the square can't keep growing along with however tall the
     panel's content gets (see .tool-panel's min-height change) -- without
     this, a tall panel (lots of tool-groups + Community Quilt) drags the
     whole section's height up with it. Raised from an earlier, tighter
     720px once .cloth-stage's isolation (above) made the actual bug that
     cap was guarding against -- pieces rendering over the sticky nav --
     structurally impossible regardless of canvas size; this number is now
     just a sanity ceiling for very tall panels, not a leak workaround. */
  max-width: 800px;
  max-height: 800px;
  background: var(--paper);
  box-shadow: inset 0 0 0 1px rgba(23, 23, 23, 0.32);
}

.template-ghost {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  z-index: 1;
}

.template-guide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 2;
}

.stitch-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 6;
}

.stitch-layer path {
  fill: none;
  stroke: var(--thread-color, var(--blue-dark));
  stroke-width: var(--thread-width, 0.18);
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0.08px 0.18px rgba(23, 23, 23, 0.32));
  vector-effect: non-scaling-stroke;
}

.template-slot {
  fill: rgba(185, 228, 243, 0.18);
  stroke: rgba(128, 109, 176, 0.22);
  stroke-width: 0.11;
  vector-effect: non-scaling-stroke;
}

/* Every piece in a guide picks up its template's own tone (the same one
   shown on the template's thumbnail), so squares and triangles in the same
   block read as one consistent color instead of a mismatched checkerboard
   of whatever each shape's fill used to default to. */
.template-guide[data-tone="blue"] .template-slot {
  fill: rgba(52, 79, 163, 0.22);
  stroke: rgba(52, 79, 163, 0.32);
}

.template-guide[data-tone="lilac"] .template-slot {
  fill: rgba(128, 109, 176, 0.24);
  stroke: rgba(128, 109, 176, 0.34);
}

.template-guide[data-tone="yellow"] .template-slot {
  fill: rgba(255, 241, 168, 0.4);
  stroke: rgba(52, 79, 163, 0.28);
}

.template-guide[data-tone="light-blue"] .template-slot {
  fill: rgba(185, 228, 243, 0.32);
  stroke: rgba(52, 79, 163, 0.28);
}

/* Log Cabin fills its whole 8x8 block with no empty margin, so the default
   single pale tint made every ring blend into one flat, invisible field.
   These tones split it into a warm diagonal (top + right logs) and a cool
   diagonal (bottom + left logs) around a distinct center square, matching
   a real log cabin's light/dark half -- the deliberate exception to the
   "one tone per template" rule above. The extra [data-tone] in the
   selector only exists to outrank it on specificity. */
.template-guide[data-tone] .template-slot.log-square-a,
.template-guide[data-tone] .template-slot.log-bar-a {
  fill: rgba(255, 241, 168, 0.4);
  stroke: rgba(52, 79, 163, 0.28);
}

.template-guide[data-tone] .template-slot.log-square-b,
.template-guide[data-tone] .template-slot.log-bar-b {
  fill: rgba(185, 228, 243, 0.45);
  stroke: rgba(52, 79, 163, 0.28);
}

/* Round 2's second color pair -- lila/blau -- so the block cycles through
   all four site colors (hellgelb, hellblau, lila, blau) across its three
   rounds instead of repeating just the one warm/cool pair throughout. */
.template-guide[data-tone] .template-slot.log-square-c,
.template-guide[data-tone] .template-slot.log-bar-c {
  fill: rgba(128, 109, 176, 0.42);
  stroke: rgba(52, 79, 163, 0.28);
}

.template-guide[data-tone] .template-slot.log-square-d,
.template-guide[data-tone] .template-slot.log-bar-d {
  fill: rgba(52, 79, 163, 0.4);
  stroke: rgba(52, 79, 163, 0.5);
}

.template-guide[data-tone] .template-slot.log-center {
  fill: rgba(255, 253, 250, 0.85);
  stroke: rgba(52, 79, 163, 0.32);
}

.template-grid-line {
  fill: none;
  stroke: rgba(52, 79, 163, 0.38);
  stroke-width: 0.18;
  vector-effect: non-scaling-stroke;
}

.cloth-stage.drag-over {
  box-shadow: inset 0 0 0 2px var(--main-hellblau);
}

.quilt-piece {
  position: absolute;
  width: 150px;
  height: 150px;
  cursor: grab;
  touch-action: none;
  filter: drop-shadow(0 14px 20px rgba(23, 23, 23, 0.12));
  transition: left 0.18s ease, top 0.18s ease, transform 0.28s ease;
  z-index: 3;
}

.grid-piece {
  appearance: none;
  border: 0;
  padding: 0;
  background: transparent;
  filter: drop-shadow(0 8px 11px rgba(23, 23, 23, 0.12));
}

.grid-piece::before {
  content: "";
  position: absolute;
  inset: 1px;
  background: var(--piece-color);
  clip-path: var(--piece-clip);
  box-shadow: inset 0 0 0 1px rgba(23, 23, 23, 0.22);
}

.grid-piece::after {
  content: none;
}

.applique-piece {
  appearance: none;
  border: 0;
  padding: 0;
  background: transparent;
  filter: drop-shadow(0 12px 16px rgba(23, 23, 23, 0.16));
  z-index: 5;
}

.applique-piece::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--piece-color);
  clip-path: var(--piece-clip);
  box-shadow: inset 0 0 0 1px rgba(23, 23, 23, 0.24);
}

.applique-piece::after {
  content: none;
}

.applique-piece.selected::before {
  box-shadow: inset 0 0 0 1px rgba(23, 23, 23, 0.38);
}

.patch-piece {
  appearance: none;
  border: 0;
  padding: 0;
  background: var(--patch-bg, var(--paper));
  box-shadow:
    inset 0 0 0 1px rgba(23, 23, 23, 0.42),
    0 12px 20px rgba(23, 23, 23, 0.12);
}

.patch-piece::before {
  content: "";
  position: absolute;
  inset: 8%;
  background: var(--patch-fg, var(--blue-dark));
  -webkit-mask: var(--patch-mask) center / contain no-repeat;
  mask: var(--patch-mask) center / contain no-repeat;
}

.quilt-piece:active {
  cursor: grabbing;
}

.quilt-piece.selected {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}

/* ---------------------------------------------------------------------
   Scroll reveal -- a light fade-and-rise for elements marked
   [data-reveal], toggled once by IntersectionObserver in site-effects.js.
   Kept to a short distance/duration so it reads as "settling into place"
   rather than a slide-in effect fighting the reader's own scroll speed.
   Respects prefers-reduced-motion by staying fully visible (see the
   media query at the very bottom of this file, and the JS's own check).
   --------------------------------------------------------------------- */
[data-reveal] {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity 0.7s ease, transform 0.7s ease;
}

[data-reveal].is-revealed {
  opacity: 1;
  transform: translateY(0);
}

/* Staggers a handful of children slightly so a row of cards/photos
   settles in left-to-right instead of all landing on the same frame. */
[data-reveal-group] > [data-reveal]:nth-child(2) { transition-delay: 0.08s; }
[data-reveal-group] > [data-reveal]:nth-child(3) { transition-delay: 0.16s; }

/* Count-up numbers (.time-stat-list strong) -- tabular-nums keeps each
   digit the same width while the value climbs, so the surrounding label
   doesn't jitter sideways as the number animates. */
[data-count-to] {
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------------
   Quilt parallax gallery -- three photos at staggered heights that drift
   apart slightly as the page scrolls (site-effects.js sets the
   translateY per column via [data-parallax-speed]). Modeled on the
   staggered photo strip from the Framer reference site Su pointed to;
   overflow:hidden on the section keeps the small scroll-driven offset
   from ever peeking into the sections above/below it.
   --------------------------------------------------------------------- */
.quilt-parallax {
  padding: clamp(40px, 6vw, 88px) max(20px, 5vw);
  background: var(--paper);
  overflow: hidden;
}

.quilt-parallax-track {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(14px, 2vw, 28px);
}

.quilt-parallax-item {
  margin: 0;
  aspect-ratio: 3 / 4;
  background: var(--paper-2);
}

.quilt-parallax-item--tall {
  aspect-ratio: 3 / 4.6;
  margin-top: clamp(24px, 4.5vw, 64px);
}

/* ---------------------------------------------------------------------
   Quote drift -- the closing statement laid directly over four detail
   photos instead of standing alone on a solid color block. Kept on the
   site's own paper tone (not black) so it still reads as this page,
   with mix-blend-mode keeping the words legible over both the pale
   label photo and the darker fold/pinwheel shots -- the way the Framer
   reference site layers its own headline over a photo collage rather
   than a solid card.
   --------------------------------------------------------------------- */
.quote-drift {
  /* Horizontal padding dropped -- the four photos now run flush to the
     viewport edge on both sides instead of sitting inset, so all four
     columns read as evenly wide right up to the border. */
  padding: clamp(64px, 10vw, 140px) 0;
  background: var(--paper-top);
  overflow: hidden;
}

.quote-drift-grid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  /* Tighter than the earlier clamp(10px,1.4vw,20px) -- a smaller, near-
     seamless gap reads as one continuous strip of photos rather than
     four separated tiles. */
  gap: clamp(2px, 0.4vw, 6px);
}

.quote-drift-item {
  margin: 0;
  aspect-ratio: 3 / 5.4;
  background: var(--paper-2);
}

/* No static offset -- all four columns start level with each other.
   The stagger itself now comes entirely from the scroll-driven parallax
   in site-effects.js (each column has its own data-parallax-speed, some
   positive and some negative), so the row reads as one even line at
   first glance and only drifts apart as the visitor scrolls, instead of
   already being offset before any scrolling happens. */

/* Outer positioning box only -- centers its child card both ways over
   the photo grid, no color or background of its own so it never fights
   with the chip's own backdrop below. */
.quote-drift-text {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(16px, 3vw, 32px);
  pointer-events: none;
  z-index: 2;
}

/* The quote itself, run straight over the photos with no card behind it --
   a solid paper chip kept it legible but read as a box dropped on top of
   the collage. Dropping the background and switching to light paper-
   colored type with a layered dark text-shadow keeps it readable over
   every photo underneath (pale label shot included) without boxing it in;
   the shadow does the contrast work that the chip used to do. */
.quote-drift-text p {
  max-width: 40ch;
  margin: 0;
  padding: 0;
  font-family: var(--body-font);
  font-size: clamp(20px, 2.8vw, 34px);
  font-weight: 600;
  font-style: italic;
  line-height: 1.4;
  text-align: center;
  color: var(--paper);
  /* Single soft shadow, matching .top-photo-caption's lighter treatment --
     was two stacked, harder shadows. */
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.45);
}

/* ---------------------------------------------------------------------
   Acquisition & Contact -- the one section repeated verbatim at the foot
   of every page (index, top, batting, backing), so a visitor who lands
   on any layer can still find out how to inquire about a quilt without
   hunting for a dedicated contact page. Each page washes it in that
   page's own two-color combo -- the same soft, translucent gradient
   pairing already used for .interview-strip and the .memory-cards top
   bars -- rather than one flat color repeated on every page. Open
   underlined rows instead of a boxed card, closer to how the Framer
   reference site lays out its own contact details than a bordered card
   would be.
   --------------------------------------------------------------------- */
.contact-section {
  background: var(--paper-top);
  /* .section-pad's bottom padding (up to 148px) is sized for a section
     that has more content below it -- as the very last thing on the
     page, that plus .contact-mark's own margin-top/padding-top (up to
     another ~104px) left a large dead strip of empty background at the
     bottom, reading as if the page hadn't finished loading. Closing
     padding cut down to something that still gives the page a clean
     edge without the extra empty scroll. */
  padding-bottom: clamp(28px, 4vw, 56px) !important;
  /* Same over-sized problem on top -- .section-pad's own top padding
     (also up to 148px) read as an oddly empty band above "Acquisition &
     Contact" once the bottom was already trimmed down, an obvious
     mismatch sitting right on top of each other. Brought down to
     roughly match the new bottom instead of the full section-pad
     amount. */
  padding-top: clamp(40px, 5vw, 84px) !important;
}

.page-top .contact-section {
  background:
    linear-gradient(135deg, rgba(128, 109, 176, 0.16), rgba(185, 228, 243, 0.24)),
    var(--paper-top);
}

.page-batting .contact-section {
  background:
    linear-gradient(135deg, rgba(52, 79, 163, 0.14), rgba(255, 241, 168, 0.26)),
    var(--paper-batting);
}

.page-backing .contact-section {
  background:
    linear-gradient(135deg, rgba(185, 228, 243, 0.24), rgba(128, 109, 176, 0.16)),
    var(--paper-backing);
}

.page-home .contact-section {
  background:
    linear-gradient(135deg, rgba(255, 241, 168, 0.28), rgba(52, 79, 163, 0.12)),
    var(--paper);
}

.contact-grid {
  display: grid;
  grid-template-columns: minmax(280px, 1.1fr) minmax(260px, 0.9fr);
  gap: clamp(24px, 4vw, 56px);
  align-items: center;
}

.contact-intro h2 {
  margin: clamp(14px, 2vw, 22px) 0 clamp(20px, 3vw, 32px);
  font-size: var(--text-h2-compact);
}

.contact-intro p:not(.section-kicker) {
  max-width: 52ch;
  margin: 0;
  font-size: var(--text-base);
  line-height: 1.55;
}

.contact-card {
  padding: 0;
  background: none;
}

.contact-card .section-kicker {
  margin: 0 0 14px;
}

.contact-name {
  margin: 0 0 22px;
  font-family: var(--mono-font);
  font-size: var(--text-h3);
}

.contact-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.contact-links li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 6px 12px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
}

.contact-links li span {
  font-family: var(--mono-font);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: rgba(23, 23, 23, 0.5);
}

.contact-links a {
  color: var(--ink);
  font-size: var(--text-base);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}

.contact-links a:hover {
  border-bottom-color: currentColor;
}

/* The small closing mark under the section -- like the copyright line
   in the Framer reference site's own footer, giving the page an actual
   endpoint instead of just stopping. */
.contact-mark {
  margin: clamp(40px, 6vw, 72px) 0 0;
  padding-top: clamp(20px, 3vw, 32px);
  border-top: 1px solid var(--line);
  font-family: var(--mono-font);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: center;
  color: rgba(23, 23, 23, 0.42);
}

@media (max-width: 1000px) {
  .site-nav {
    grid-template-columns: 1fr auto;
    height: auto;
    min-height: 58px;
    row-gap: 8px;
    padding-block: 10px;
  }

  .nav-links {
    order: 3;
    grid-column: 1 / -1;
    justify-self: stretch;
    justify-content: space-between;
  }

  .issue {
    grid-column: 2;
  }

  .layer-hero,
  .home-hero,
  .blanket-scroll,
  .home-sandwich,
  .home-closing,
  .manifesto-grid,
  .timeline-section,
  .editorial-pair,
  .aunt-archive,
  .transcript-intro,
  .contact-grid,
  .sensory,
  .transcript-qa,
  .transcript-media,
  .transcript-close,
  .transcript-close-body,
  .workshop-invite,
  .workshop-trace-body,
  .quilt-tool,
  .tool-cta,
  .practice-quilt,
  .tempo-note,
  .social-note {
    grid-template-columns: 1fr;
  }

  .workshop-invite-video,
  .social-note-video {
    max-height: 60svh;
  }

  .layer-hero {
    height: auto;
  }

  .hero-copy {
    grid-template-rows: auto auto auto;
    align-content: start;
  }

  .hero-deck {
    min-height: 0;
    overflow: visible;
  }

  .hero-media {
    min-height: 62svh;
    height: auto;
  }

  .home-logo-wrap,
  .sandwich-intro {
    min-height: 0;
  }

  .home-logo-wrap {
    padding-top: 28px;
  }

  /* .home-hero's gap and .home-copy .eyebrow's margin-top no longer need
     overriding here -- below 1000px they're just the safe base values
     (see those rules near the top of the file), since the >=1000px
     query is what opts back into the tighter, vw-based spacing where
     .home-logo-wrap's min-height cushion (above) is actually present.

     The margin-top: 100svh this section used to carry (guaranteeing the
     text couldn't scroll into view until the logo had fully landed)
     traded the overlap for an oversized empty gap above the text and a
     cramped one below it before the next section -- removed per
     feedback in favor of the compact spacing back. The overlap itself
     was really two stacked bugs in fold-intro.js: a stale cached target
     position (fixed -- targetCenter() is no longer cached) causing jerky
     "down, then back up" motion, and left/top-based positioning forcing
     a layout recalculation every scroll frame (fixed -- now
     transform-based, GPU-composited). With the motion itself now
     correct and smooth instead of janky, a brief pass of the logo over
     this text during the scroll-in should read as quick motion rather
     than a stuck, broken-looking overlap. */
  .home-copy {
    align-self: start;
    padding-bottom: 20px;
  }

  .timeline-intro {
    position: static;
    min-height: 0;
  }

  /* No longer a slim sticky sidebar at this width -- let the paragraph
     use the full stacked-column measure instead of the desktop 38ch
     sidebar cap, so justify has enough words per line. */
  .timeline-intro p:last-child {
    max-width: 60ch;
  }

  .transcript-pull,
  .transcript-qa .question,
  .transcript-qa .answer {
    grid-column: 1;
  }

  .timeline-grid,
  .memory-cards,
  .interview-strip,
  .warm-gallery {
    grid-template-columns: repeat(2, 1fr);
  }

  .sensory figure {
    grid-column: auto;
  }

  .aunt-hands {
    grid-column: auto;
  }

  .blanket-scroll-head {
    min-height: 0;
    gap: 18px;
  }

  .quilt-tool {
    min-height: auto;
  }

  .tool-panel {
    max-height: none;
    height: auto;
    min-height: 0;
    order: 2;
    overflow: visible;
  }

  .cloth-stage-wrap {
    order: 1;
  }
}

@media (max-width: 680px) {
  .site-nav {
    padding-inline: 14px;
  }

  .brand span:last-child,
  .issue {
    display: none;
  }

  .interview-strip,
  .blanket-scroll,
  .timeline-grid,
  .memory-cards,
  .warm-gallery,
  .section-heading,
  .quilt-parallax-track,
  .community-result-grid {
    grid-template-columns: 1fr;
  }

  .quilt-parallax-item--tall {
    aspect-ratio: 3 / 4;
    margin-top: 0;
  }

  /* Two columns instead of four -- narrow enough that each photo still
     reads clearly on a phone, but keeps the 2x2 block the sentence
     overlay needs to sit across (see .quote-drift-text below, which goes
     back to the desktop absolute/inset:0 overlay here instead of
     dropping out as its own stacked text block). */
  .quote-drift-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  .quote-drift-item {
    aspect-ratio: 3 / 4;
  }

  .quote-drift-text p {
    max-width: 28ch;
  }

  /* Below this width .timeline-intro's own padding was stacking on top
     of the section's outer padding, squeezing the paragraph down to
     roughly 300px -- barely enough words per line for justify to read
     calmly. Dropping the inner side padding (the section's own padding
     already gives it a clean edge) gives the text noticeably more room
     to work with. Same fix for .pair-copy, which has the identical
     nested-padding setup inside .editorial-pair. */
  .timeline-intro,
  .pair-copy {
    padding-inline: 0;
  }

  .timeline-grid article,
  .editorial-pair figure {
    min-height: clamp(360px, 90vw, 560px);
  }

  .aunt-archive figure,
  .warm-gallery figure,
  .home-closing figure {
    min-height: clamp(260px, 72vw, 430px);
  }

  /* Compact on mobile, matching the tight spacing in the reference shot --
     the overlap during scroll-in turned out to be a real animation bug
     (targetCenter() was caching a stale screen position instead of
     re-measuring the slot's live, scrolling location -- fixed in
     fold-intro.js), not something caused by this section's height, so
     there's no need to inflate it just to create distance. */
  .home-hero {
    min-height: auto;
    padding-top: 32px;
  }

  .home-copy h1 {
    font-size: clamp(48px, 18vw, 92px);
  }

  /* The layer-hero pixel titles ("Quilting / Through / Time" etc.) were
     pinned to the desktop clamp's 54px floor on every phone -- 9vw only
     clears 54px above ~600px width, so on a real phone it never grew and
     read as noticeably smaller than the rest of the page. Bumped the
     vw multiplier for mobile specifically. Ceiling picked from the
     actual measured glyph width of "Quilting" (the widest of the three
     stacked words, ~4em across in this font) so it keeps clearing the
     column at the narrowest phones (~360px) without wrapping mid-word or
     overflowing -- checked against a live 398px-wide render before
     landing on 80px as the safe top end. */
  .hero-copy h1 {
    font-size: clamp(56px, 20vw, 80px);
  }

  .blanket-scroll-head,
  .blanket-track {
    grid-template-columns: 1fr;
  }

  .blanket-track figure:nth-child(even) {
    margin-top: 0;
  }

  /* The Country Bears negative nudge only makes sense in the two-column
     desktop grid, where it pulls the image up into its own gutter --
     in the single-column mobile stack it would just overlap the item
     above it, so it's neutralized here. */
  .blanket-track figure:nth-child(9) {
    margin-top: 0;
  }

  /* Single-column stack on mobile -- every figure reads full-width, so
     the even column's right-alignment (meant to answer the two-column
     zigzag) no longer has a partner to zigzag against and just reads
     left like everything else. */
  .blanket-track figure:nth-child(even) figcaption {
    text-align: left;
  }

  .interview-strip p {
    min-height: 150px;
  }

  .sandwich-visual-panel {
    min-height: 340px;
  }

  /* Desktop keeps the illustration at a fixed 54% width, left-anchored at
     9%, to leave room on the right for .sandwich-visual-word's hover
     label beside it. On the stacked mobile layout that left-over 46% just
     reads as dead space -- the panel's own 340px min-height (above) is
     far taller than a 54%-wide illustration's own height (its box is
     locked to the source art's ratio), so most of the panel sat empty.
     Widened and re-centered so it actually fills the panel instead. */
  .sandwich-hotspot,
  .sandwich-visual-frame {
    left: 50%;
    width: 82%;
    transform: translate(-50%, -50%);
  }

  .sandwich-visual-word {
    font-size: clamp(20px, 6vw, 34px);
  }

  .sensory figure {
    aspect-ratio: 4 / 5;
  }

  .patch-palette {
    grid-template-columns: repeat(4, 1fr);
    max-height: none;
  }

  .tool-panel,
  .cloth-stage-wrap {
    padding: 14px;
  }

  /* Shape and Color sit side by side on desktop (flex, ~1.3:1 split) --
     fine when the panel column has 500+px to give each half, but on a
     phone-width panel that leaves Color's own half too narrow for its
     2-column swatch grid (each swatch needs ~100px+ to fit a full label
     like "Hellgelb" next to its color box without crowding/clipping).
     Stacking them removes the side-by-side squeeze entirely -- Color
     gets the panel's full width for its own row, same as Shape above it. */
  .tool-group-pair {
    flex-direction: column;
    /* Desktop's align-items: start keeps the two side-by-side columns
       top-aligned instead of stretched to match each other's height --
       switched to a column layout above, that same "start" now controls
       the CROSS axis instead, which is horizontal here, so each group
       shrank to its own content width (Color's swatch grid floating at
       ~200px on the left) rather than filling the row. Stretch makes
       Shape and Color both run the panel's full width once stacked. */
    align-items: stretch;
  }

  /* .top-photo-feature's figure is width:100% + aspect-ratio:3/2 -- on a
     narrow phone that only works out to a short ~265px strip, most of
     which the overlay card's own content (kicker + heading + a full
     paragraph) is taller than. Since the card is position:absolute, it
     doesn't affect the section's height and just overflows past that
     short photo, spilling into whatever comes next in the document --
     the photo itself ends up reduced to a thin, barely-visible sliver
     peeking out above the card. Sizing the section itself (nav-aware,
     same convention as .layer-hero) and letting the figure fill it
     completely instead of following the 3/2 ratio turns this into a
     proper full-screen photo moment, matching what the desktop version
     already reads as at typical laptop widths, with the card comfortably
     inset over real photo on every side instead of overflowing it. */
  .top-photo-feature {
    min-height: calc(100svh - 58px);
  }

  .top-photo-feature figure {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    aspect-ratio: auto;
  }

  /* The card was still anchored to the top (right under the nav) and
     sized for a full paragraph at desktop scale -- on a phone-height
     photo that meant the card alone covered most of the frame, leaving
     only a sliver of the actual photo visible above it. Moving it to
     the bottom edge and trimming padding/type size keeps it legible
     while giving the photo itself the majority of the screen again. */
  .top-photo-caption {
    top: auto;
    bottom: max(16px, 4vw);
    left: max(16px, 4vw);
    right: max(16px, 4vw);
    max-width: none;
    padding: 16px;
  }

  .top-photo-feature .top-photo-caption h2 {
    margin: 6px 0 10px;
    font-size: clamp(20px, 6vw, 28px);
  }

  .top-photo-caption p:last-child {
    font-size: 14px;
    line-height: 1.45;
  }

  .form-palette,
  .fabric-palette {
    grid-template-columns: repeat(4, 1fr);
  }

  .template-palette {
    grid-template-columns: repeat(3, 1fr);
    max-height: none;
  }

  .segmented {
    grid-template-columns: repeat(2, 1fr);
  }

  .range-row {
    grid-template-columns: 82px 1fr;
  }

  .cloth-stage.grid-cloth {
    min-height: 0;
    aspect-ratio: 1;
  }
}

/* Belt-and-suspenders alongside the prefers-reduced-motion check already
   in site-effects.js -- if CSS loads before JS runs, a reduced-motion
   visitor still never sees the faded-out starting state. */
@media (prefers-reduced-motion: reduce) {
  [data-reveal] {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* --- Community Quilt admin (admin.html) ---------------------------------
   Not linked from the public site -- reached only by URL, and gated by
   real Supabase Auth sign-in (see admin.js), not just obscurity. Reuses
   the site's own tokens/typography but is otherwise a plain utility
   screen, since it's a tool for Su, not a page visitors are meant to see. */

.admin-main {
  max-width: 960px;
  margin: 0 auto;
  padding: clamp(32px, 6vw, 64px) max(20px, 4vw) 96px;
}

.admin-login h1,
.admin-panel h1 {
  margin: 4px 0 12px;
  font-size: clamp(28px, 4vw, 44px);
  color: var(--main-blue);
}

.admin-note {
  max-width: 56ch;
  color: var(--ink);
  opacity: 0.72;
  font-size: 13px;
  line-height: 1.5;
}

.admin-login-form {
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-width: 340px;
  margin-top: 20px;
}

.admin-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 11px;
  font-weight: 760;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink);
}

.admin-field input {
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--paper);
  font-family: inherit;
  font-size: 14px;
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: var(--ink);
}

.admin-login-button {
  align-self: flex-start;
  padding: 12px 22px;
  border: 0;
  border-radius: 999px;
  background: var(--main-blue);
  color: var(--paper);
  font-size: 11px;
  font-weight: 780;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
}

.admin-login-button:disabled {
  opacity: 0.5;
  cursor: default;
}

.admin-status {
  margin-top: 14px;
  min-height: 15px;
  font-size: 12px;
  font-weight: 700;
  color: var(--ink);
}

.admin-status.is-error {
  color: #a33b3b;
}

.admin-login.is-hidden,
.admin-panel.is-hidden {
  display: none;
}

.admin-panel-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
}

.admin-signout {
  padding: 10px 18px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--paper);
  font-size: 11px;
  font-weight: 760;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  cursor: pointer;
  flex: none;
}

.admin-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 16px;
  margin-top: 24px;
}

.admin-grid figure {
  margin: 0;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  background: var(--paper);
}

.admin-grid img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
}

.admin-grid figcaption {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 10px;
  font-size: 11px;
  line-height: 1.3;
}

.admin-grid figcaption span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.admin-delete {
  flex: none;
  padding: 5px 10px;
  border: 0;
  border-radius: 999px;
  background: #a33b3b;
  color: var(--paper);
  font-size: 10px;
  font-weight: 760;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
}

.admin-empty {
  margin-top: 24px;
  font-size: 13px;
  color: var(--ink);
  opacity: 0.6;
}

.admin-empty.is-hidden {
  display: none;
}

/* Idle screensaver -- built and appended by site-effects.js after a
   stretch of no mouse/keyboard/touch activity. Fixed + fully opaque
   background behind the photo so nothing from the page shows through
   the moment it fades in; opacity transition (not display toggling) is
   what makes the fade actually visible, and pointer-events stays off so
   it never has to intercept the very interaction that's meant to
   dismiss it. */
.idle-screensaver {
  position: fixed;
  inset: 0;
  z-index: 999;
  background: var(--ink, #171717);
  opacity: 0;
  pointer-events: none;
  transition: opacity 900ms ease;
}

.idle-screensaver.is-visible {
  opacity: 1;
}

.idle-screensaver img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transform: scale(1);
}

/* Slow, continuous Ken Burns breathing -- eases in to 1.08x and back out
   to 1x, on a loop, for as long as the screensaver stays up (was a single
   one-way push-in via transition before; now zooms both in and out). */
@keyframes idle-screensaver-breathe {
  0%,
  100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.08);
  }
}

@media (prefers-reduced-motion: no-preference) {
  .idle-screensaver.is-visible img {
    animation: idle-screensaver-breathe 26000ms ease-in-out infinite;
  }
}

/* Small pill hint, same rounded/uppercase treatment as .playground-jump,
   so it reads as an intentional piece of UI rather than a stray label
   floating on the photo. Delayed and slower fade than the photo itself
   so it arrives a beat after the image, not simultaneously with it. */
.idle-screensaver-hint {
  position: absolute;
  left: 50%;
  bottom: clamp(28px, 6vw, 64px);
  transform: translateX(-50%);
  margin: 0;
  padding: 11px 20px;
  border: 1px solid rgba(255, 253, 250, 0.55);
  border-radius: 999px;
  background: rgba(23, 23, 23, 0.22);
  color: var(--paper);
  font-size: 11px;
  font-weight: 780;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.45);
  opacity: 0;
  transition: opacity 900ms ease 500ms;
}

.idle-screensaver.is-visible .idle-screensaver-hint {
  opacity: 1;
}
