/* ============================================================
   MAISON N°7 — The Composition Table
   Design tokens

   Direction: a night atelier, not a shopfront. Garments are shot
   flat (product photography, not a mannequin), so the whole scene
   is built around that truth — a lit table the pieces are laid on,
   each in its own pool of lamp-light, rather than faking a body
   that was never photographed. Near-black walnut ground, warm
   paper for the two light pools, a single brass accent pulled from
   real hardware, nothing terracotta, nothing SaaS-card.
   ============================================================ */
:root {
  --room:        #131110;
  --room-deep:   #0b0a09;
  --table:       #1c1815;
  --table-edge:  #262019;
  --paper:       #f4ecdd;
  --paper-dim:   #e7ddc7;
  --ink:         #efe9dc;
  --ink-dim:     #a99f8d;
  --ink-faint:   #6f6656;
  --line:        #35302a;
  --line-soft:   rgba(244, 236, 221, 0.09);
  --brass:       #c9924f;
  --brass-bright:#e8b26f;
  --brass-dim:   #8a6636;

  --font-display: 'Fraunces', Georgia, serif;
  --font-ui: 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-mono: 'Space Mono', ui-monospace, monospace;

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-soft: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
}

body {
  background: var(--room);
  color: var(--ink);
  font-family: var(--font-ui);
  -webkit-font-smoothing: antialiased;
  position: relative;
  cursor: default;
  overflow-x: hidden;
}

img { display: block; }

button {
  font-family: inherit;
  color: inherit;
}

::selection {
  background: var(--brass-dim);
  color: var(--paper);
}

/* Film grain + vignette give the near-black room depth instead of
   reading as flat #000 — the one atmospheric constant across every
   surface. */
.grain {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 80;
  opacity: 0.4;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.35'/%3E%3C/svg%3E");
}

.vignette {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 79;
  background: radial-gradient(ellipse 90% 75% at 50% 38%, transparent 55%, rgba(0,0,0,0.55) 100%);
}

/* Custom brass pin cursor — only shown over the table, tracked from JS.
   A quiet signature touch rather than scattered hover gimmicks. */
.pin-cursor {
  position: fixed;
  top: 0;
  left: 0;
  width: 22px;
  height: 22px;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 90;
  opacity: 0;
  transition: opacity 0.25s var(--ease-soft), transform 0.05s linear;
}

.pin-cursor svg {
  width: 100%;
  height: 100%;
  fill: none;
  stroke: var(--brass-bright);
  stroke-width: 1.4;
  filter: drop-shadow(0 0 6px rgba(232, 178, 111, 0.65));
}

.pin-cursor.is-active { opacity: 1; }
.pin-cursor.is-down svg circle { fill: var(--brass-bright); }

@media (hover: none) {
  .pin-cursor { display: none; }
}

/* ============================================================
   Ambient dust field
   True suspended dust, not embers: a couple dozen brass-lit
   specks that hang roughly where they're placed and idle inside a
   small private radius — a slow, uneven wobble on X and Y, each on
   its own unsynchronized loop, with a lazy breathing fade rather
   than a hard on/off. Nothing crosses the room and nothing has a
   destination; it just sits in the lamp-light the way real dust
   does, catching the light and drifting a few pixels off true.
   Populated once by initDustField(); the field itself also carries
   a gentle parallax offset written by the same function, so the
   whole layer breathes with scroll position rather than looping in
   place regardless of what the reader is doing.
   ============================================================ */
.dust-field {
  position: fixed;
  inset: 0;
  z-index: 3;
  overflow: hidden;
  pointer-events: none;
  mix-blend-mode: screen;
  transform: translateY(var(--dust-drift, 0px));
  transition: transform 0.4s linear;
}

.dust-mote {
  position: absolute;
  top: var(--y, 50%);
  left: var(--x, 50%);
  width: var(--size, 2.6px);
  height: var(--size, 2.6px);
  border-radius: 50%;
  background: var(--brass-bright);
  filter: blur(0.5px);
  box-shadow: 0 0 6px 1px rgba(232, 178, 111, 0.5);
  opacity: 0;
  animation:
    dust-hover var(--dur, 14s) ease-in-out var(--delay, 0s) infinite,
    dust-breathe var(--fade-dur, 9s) ease-in-out var(--fade-delay, 0s) infinite;
}

/* A small, lazy, non-repeating-looking wander inside the mote's own
   private radius — six waypoints so the loop doesn't read as an
   obvious back-and-forth tween, still small enough to feel like
   still air rather than a current. */
@keyframes dust-hover {
  0%   { transform: translate(0, 0); }
  20%  { transform: translate(calc(var(--wx, 14px) * 0.4), calc(var(--wy, 14px) * -0.7)); }
  40%  { transform: translate(calc(var(--wx, 14px) * -0.6), calc(var(--wy, 14px) * -0.3)); }
  60%  { transform: translate(calc(var(--wx, 14px) * -0.3), calc(var(--wy, 14px) * 0.8)); }
  80%  { transform: translate(calc(var(--wx, 14px) * 0.7), calc(var(--wy, 14px) * 0.35)); }
  100% { transform: translate(0, 0); }
}

/* Independent fade cycle (different duration than the hover drift,
   so the two never lock into a visible repeat together) — a mote
   catches the light, holds, and dims again, rather than blinking. */
@keyframes dust-breathe {
  0%, 100% { opacity: 0; }
  50%      { opacity: var(--peak, 0.45); }
}

@media (max-width: 640px), (prefers-reduced-motion: reduce) {
  .dust-field { display: none; }
}

/* ============================================================
   Seal watermark
   A small stand-in for the masthead seal, hidden until the real
   one scrolls out of view, then holding a quiet, continuous spin
   in the corner for the rest of the page — proof the page is still
   "on" no matter how far down the reader has gone.
   ============================================================ */
.seal-watermark {
  position: fixed;
  top: 20px;
  left: 22px;
  width: 38px;
  height: 38px;
  z-index: 60;
  opacity: 0;
  transform: scale(0.7) rotate(-12deg);
  transition: opacity 0.5s var(--ease-soft), transform 0.5s var(--ease-soft);
  pointer-events: none;
}

.seal-watermark.is-visible {
  opacity: 0.85;
  transform: scale(1) rotate(0deg);
}

.seal-watermark svg {
  width: 100%;
  height: 100%;
  animation: seal-spin 40s linear infinite;
}

.seal-watermark circle {
  fill: none;
  stroke: var(--brass-dim);
  stroke-width: 1.2;
}

.seal-watermark text {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 500;
  font-size: 13px;
  fill: var(--brass-bright);
}

@media (max-width: 640px) {
  .seal-watermark { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .seal-watermark { transition: none; }
  .seal-watermark svg { animation: none; }
}

/* ============================================================
   Scroll spine
   A brass measuring thread run down the page edge — this site's
   own idea of a scroll progress bar, styled like the tailor's tape
   the rest of the interface keeps referencing (table ruler ticks,
   the 1∶4 scale readout). A pin eases toward the reader's actual
   position; tick marks along the thread name and light up for each
   section as it's passed. Built entirely from script.js.
   ============================================================ */
.scroll-spine {
  position: fixed;
  top: 12vh;
  bottom: 12vh;
  right: 30px;
  width: 1px;
  z-index: 45;
  pointer-events: none;
}

.scroll-spine__track {
  position: absolute;
  inset: 0;
  background: var(--line-soft);
}

.scroll-spine__fill {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 0%;
  background: linear-gradient(180deg, var(--brass-dim), var(--brass-bright));
  box-shadow: 0 0 8px rgba(232, 178, 111, 0.45);
}

.scroll-spine__tick {
  position: absolute;
  right: -3px;
  width: 7px;
  height: 1px;
  background: var(--ink-faint);
  color: var(--ink-faint);
  transition: background 0.3s var(--ease-soft), color 0.3s var(--ease-soft), box-shadow 0.3s var(--ease-soft);
}

.scroll-spine__tick.is-passed {
  background: var(--brass-bright);
  color: var(--brass-bright);
  box-shadow: 0 0 6px rgba(232, 178, 111, 0.6);
}

.scroll-spine__tick::after {
  content: attr(data-label);
  position: absolute;
  right: 14px;
  top: 50%;
  transform: translateY(-50%);
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.03em;
  color: inherit;
  opacity: 0.75;
  white-space: nowrap;
}

.scroll-spine__pin {
  position: absolute;
  top: 0%;
  right: -6px;
  width: 13px;
  height: 13px;
  transform: translate(50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, var(--brass-bright), var(--brass-dim) 75%);
  box-shadow: 0 0 10px rgba(232, 178, 111, 0.7), 0 1px 2px rgba(0, 0, 0, 0.5);
}

.scroll-spine__readout {
  position: absolute;
  top: 0%;
  right: 15px;
  transform: translateY(-50%);
  font-family: var(--font-mono);
  font-size: 0.66rem;
  color: var(--brass-bright);
  white-space: nowrap;
}

@media (max-width: 1180px) {
  .scroll-spine { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .scroll-spine__pin,
  .scroll-spine__readout {
    transition: none;
  }
}

/* ============================================================
   Layout shell
   ============================================================ */
.app {
  max-width: 1320px;
  margin: 0 auto;
  padding: 72px 44px 44px;
  position: relative;
  z-index: 1;
}

/* ============================================================
   Masthead
   ============================================================ */
.masthead {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "id      actions"
    "desc    actions";
  align-items: start;
  column-gap: 48px;
  row-gap: 22px;
  margin-bottom: 64px;
  padding-bottom: 36px;
  border-bottom: 1px solid var(--line);
}

.masthead__id {
  grid-area: id;
  display: flex;
  align-items: center;
  gap: 26px;
}

.masthead__seal {
  flex: none;
  width: 76px;
  height: 76px;
  opacity: 0;
  animation: seal-in 0.9s var(--ease-out) 0.15s forwards;
}

@keyframes seal-in {
  from { opacity: 0; transform: scale(0.7) rotate(-14deg); }
  to   { opacity: 1; transform: scale(1) rotate(0deg); }
}

.masthead__seal svg {
  width: 100%;
  height: 100%;
  animation: seal-spin 40s linear infinite;
}

@keyframes seal-spin {
  to { transform: rotate(360deg); }
}

.masthead__seal circle {
  fill: none;
  stroke: var(--brass-dim);
  stroke-width: 0.6;
}

.masthead__seal text:not(textPath) {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 500;
  font-size: 11px;
  fill: var(--brass-bright);
}

.masthead__seal textPath {
  fill: var(--ink-faint);
  font-family: var(--font-mono);
}

@media (prefers-reduced-motion: reduce) {
  .masthead__seal { animation: none; opacity: 1; }
  .masthead__seal svg { animation: none; }
}

.masthead__copy {
  opacity: 0;
  animation: rise-in 0.8s var(--ease-out) 0.3s forwards;
}

@keyframes rise-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}

.masthead__kicker {
  margin: 0 0 8px;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
}

.masthead__title {
  font-family: var(--font-display);
  font-weight: 380;
  font-size: clamp(1.9rem, 3.1vw, 2.6rem);
  line-height: 1.08;
  margin: 0;
  letter-spacing: -0.01em;
  color: var(--ink);
}

/* Each word sits in its own overflow-hidden window so its inner
   span can rise up from below the baseline into view, like type
   being set line by line rather than the whole headline just
   fading in at once. Built by wrapTitleWords() in script.js; if
   that JS never runs, the title is still plain, fully visible text
   — nothing here depends on JS to become readable. */
.masthead__title .word {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
}

.masthead__title .word__inner {
  display: inline-block;
  transform: translateY(112%);
  animation: word-rise 0.85s var(--ease-out) forwards;
  animation-delay: calc(0.5s + var(--i, 0) * 55ms);
}

@keyframes word-rise {
  to { transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .masthead__title .word__inner {
    animation: none;
    transform: none;
  }
}

.masthead__desc {
  grid-area: desc;
  margin: 0;
  max-width: 46ch;
  color: var(--ink-dim);
  font-size: 0.96rem;
  line-height: 1.65;
  opacity: 0;
  animation: rise-in 0.8s var(--ease-out) 0.45s forwards;
}

.masthead__actions {
  grid-area: actions;
  display: flex;
  gap: 10px;
  align-items: center;
  opacity: 0;
  animation: rise-in 0.8s var(--ease-out) 0.55s forwards;
}

.knob {
  border: 1px solid var(--line);
  background: var(--table);
  color: var(--ink-dim);
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 13px 16px;
  font-size: 0.8rem;
  font-family: var(--font-ui);
  letter-spacing: 0.01em;
  cursor: pointer;
  border-radius: 3px;
  transition: border-color 0.2s var(--ease-soft), color 0.2s var(--ease-soft), background 0.2s var(--ease-soft), transform 0.15s var(--ease-spring);
  white-space: nowrap;
  /* --mx/--my are written on mousemove by initMagneticKnobs() — the
     button leans slightly toward the pointer instead of just sitting
     there waiting to be clicked. Default 0px keeps touch/keyboard
     users at rest. */
  transform: translate(var(--mx, 0px), var(--my, 0px));
}

.knob svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: none;
}

.knob:not(.knob--wide) span { display: none; }

.knob:hover {
  border-color: var(--brass-dim);
  color: var(--brass-bright);
}

.knob:active {
  transform: translate(var(--mx, 0px), var(--my, 0px)) scale(0.94);
}

.knob--wide {
  background: var(--brass);
  border-color: var(--brass);
  color: var(--room-deep);
  font-weight: 500;
}

.knob--wide:hover {
  background: var(--brass-bright);
  border-color: var(--brass-bright);
  color: var(--room-deep);
}

.knob:focus-visible {
  outline: 2px solid var(--brass-bright);
  outline-offset: 2px;
}

/* Worn-together toggle reads as a pressed state, not a one-shot
   action button like shuffle or reset — brass fill only while active. */
.knob--toggle svg { fill: none; }

.knob--toggle[aria-pressed="true"] {
  background: var(--brass);
  border-color: var(--brass);
  color: var(--room-deep);
  font-weight: 500;
}

.knob--toggle[aria-pressed="true"]:hover {
  background: var(--brass-bright);
  border-color: var(--brass-bright);
}

.knob--toggle span { display: inline; }

@media (max-width: 560px) {
  .knob--toggle span { display: none; }
}

/* Little rotate flourish on the reset icon when clicked */
.knob.is-spinning svg {
  animation: knob-spin 0.5s var(--ease-out);
}

@keyframes knob-spin {
  to { transform: rotate(-320deg); }
}

/* Tooltip via data-tip, no extra markup */
.knob[data-tip] {
  position: relative;
}

.knob[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  top: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%) translateY(-4px);
  background: var(--room-deep);
  border: 1px solid var(--line);
  color: var(--ink-dim);
  font-size: 0.7rem;
  padding: 6px 10px;
  border-radius: 3px;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s var(--ease-soft), transform 0.18s var(--ease-soft);
  z-index: 20;
}

.knob[data-tip]:hover::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .masthead__copy, .masthead__desc, .masthead__actions { animation: none; opacity: 1; }
}

/* ============================================================
   Workspace
   ============================================================ */
.workspace {
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: 56px;
  align-items: start;
  transform: scale(var(--exit-scale, 1));
  transform-origin: 50% 0%;
  transition: transform 0.1s linear;
}

@media (prefers-reduced-motion: reduce) {
  .workspace { transform: none; }
}

/* ============================================================
   THE TABLE
   A lit walnut surface seen from a shallow angle. Each garment
   sits flat in its own pool of lamp-light — true to how it was
   actually photographed — with a soft cast shadow and a gentle
   3D tilt that responds to the pointer, instead of pretending
   the two product shots are one worn body.
   ============================================================ */
.table-wrap {
  display: flex;
  flex-direction: column;
  gap: 14px;
  transform: perspective(1600px) rotateX(var(--scroll-tilt, 0deg));
  transition: transform 0.1s linear;
}

.table-scene {
  position: relative;
  aspect-ratio: 8 / 7;
  border-radius: 6px;
  overflow: hidden;
  background: var(--table);
  perspective: 1600px;
  border: 1px solid var(--line);
  box-shadow:
    0 60px 120px -40px rgba(0,0,0,0.7),
    inset 0 0 0 1px rgba(244,236,221,0.03);
}

.table-surface {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Real walnut photo as the base layer, with the original gradient
     trio kept exactly as they were but turned semi-transparent, so
     the same left-to-right depth falloff and fine grain lines still
     read on top of the photo instead of being replaced by it. */
  background:
    radial-gradient(ellipse 130% 90% at 50% -10%, rgba(244,236,221,0.10), transparent 55%),
    repeating-linear-gradient(100deg, rgba(244,236,221,0.018) 0px, rgba(244,236,221,0.018) 1px, transparent 1px, transparent 68px),
    linear-gradient(155deg, rgba(28,24,21,0.55) 0%, rgba(38,32,25,0.45) 55%, rgba(23,19,16,0.65) 100%),
    url('assets/images/table/walnut-grain.jpg');
  background-size: auto, auto, auto, cover;
  background-position: center, center, center, center;
  background-repeat: no-repeat;
}

/* Page-load scrim: the table starts almost dark, then this fades
   out as the lamp warms up, so there's an actual "arrival" moment
   instead of the scene simply being lit from the first frame.
   Driven by --entrance-scrim (1 → 0) from initEntranceSequence().
   Defaults to 0 (invisible) so the table looks correct if JS never
   runs or the entrance is skipped for reduced motion. */
.table-entrance-scrim {
  position: absolute;
  inset: 0;
  z-index: 8;
  pointer-events: none;
  background: #000000;
  opacity: var(--entrance-scrim, 0);
  transition: opacity 0.1s linear;
}

.table-seam {
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 1px;
  background: linear-gradient(180deg, transparent, rgba(244,236,221,0.06) 20%, rgba(244,236,221,0.06) 80%, transparent);
  transform: translateX(-50%);
  pointer-events: none;
}

.lamp-glow {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse 60% 40% at 50% 8%, rgba(244,236,221,0.14), transparent 60%);
  mix-blend-mode: screen;
  /* Final opacity = entrance progress × scroll intensity, so the
     page-load warm-up and the existing scroll dimming compose
     instead of one overriding the other. --lamp-entrance defaults
     to 1 (fully on) so the lamp still works correctly if JS never
     runs or reduced-motion skips the entrance sequence. */
  opacity: calc(var(--lamp-entrance, 1) * var(--lamp-intensity, 1));
  transition: opacity 0.1s linear;
}

.table-plate {
  position: absolute;
  top: 22px;
  left: 24px;
  z-index: 6;
  margin: 0;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.02em;
  color: var(--brass-bright);
}

.table-plate span {
  font-weight: 700;
}

.table-scale {
  position: absolute;
  top: 22px;
  right: 24px;
  z-index: 6;
  margin: 0;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.02em;
  color: var(--ink-faint);
  text-transform: capitalize;
}

.table-ruler {
  position: absolute;
  pointer-events: none;
  opacity: 0.5;
  background-repeat: repeat;
}

.table-ruler--top {
  top: 0;
  left: 0;
  right: 0;
  height: 14px;
  background-image: repeating-linear-gradient(90deg, var(--line-soft) 0 1px, transparent 1px 24px);
}

.table-ruler--left {
  top: 0;
  bottom: 0;
  left: 0;
  width: 14px;
  background-image: repeating-linear-gradient(180deg, var(--line-soft) 0 1px, transparent 1px 24px);
}

/* Each bay is a pool of paper-light the garment rests in. Sized as
   an inset box with real breathing room on every side, so the
   scaled garment (which can range from 60%-160% fit-scale on top of
   its own normalize factor) has headroom to grow or shrink without
   ever touching the bay edge. */
.garment-bay {
  position: absolute;
  top: 13%;
  bottom: 11%;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: visible;
}

.garment-bay--jacket { left: 6%;  width: 40%; }
.garment-bay--pants  { right: 6%; width: 40%; }

.garment-pool {
  position: absolute;
  inset: 4% 2%;
  border-radius: 4px;
  background: radial-gradient(ellipse 80% 70% at 50% 42%, rgba(244,236,221,0.065), transparent 72%);
  pointer-events: none;
}

.garment-shadow {
  position: absolute;
  left: 50%;
  bottom: -2%;
  width: 56%;
  height: 9%;
  transform: translateX(-50%);
  background: radial-gradient(ellipse 50% 100% at 50% 50%, rgba(0,0,0,0.6), transparent 72%);
  filter: blur(3px);
  z-index: 1;
  transition: width 0.4s var(--ease-soft), opacity 0.4s var(--ease-soft);
}

.garment-tilt {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  transform-style: preserve-3d;
  transform: rotateX(6deg) rotateY(-3deg);
  transition: transform 0.5s var(--ease-out);
  z-index: 2;
}

/* Base size is capped to fit fully inside the bay (object-fit:
   contain, effectively) BEFORE any fit-scale multiplier is applied.
   JS's scale(x, y) transform then grows or shrinks from that
   contained baseline, so 100% fit-scale reads as "fills its bay
   naturally" and the 60–160% range moves relative to that, instead
   of relative to an un-contained, overflowing raw image. */
.garment {
  height: 100%;
  width: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  object-position: center center;
  filter:
    drop-shadow(0 2px 1px rgba(0,0,0,0.25))
    drop-shadow(0 18px 30px rgba(0,0,0,0.45));
  transform-origin: center center;
  transition: opacity 0.3s var(--ease-soft), filter 0.35s var(--ease-soft);
  will-change: transform;
  /* The <img> itself never receives the drag listeners (see
     script.js) — its native browser image-drag ghost still needs
     turning off, or a press can kick off an OS-level "drag this
     image out" instead of our own drag. */
  -webkit-user-drag: none;
  user-select: none;
  -webkit-user-select: none;
}

/* Hand-draggable garments: a visible grab affordance on the
   container that actually owns the drag. */
.garment-tilt.is-draggable {
  cursor: grab;
  touch-action: none;
}

.garment-tilt.is-dragging {
  cursor: grabbing;
}

/* Entry flourish when a new piece is chosen: a fold-out settle
   rather than a generic drop-and-fade. The garment arrives as if
   still lifted from a stacked shelf — canted on its long axis and
   compressed along one edge, like cloth held by two corners — then
   unfurls flat in one motion as it's laid down, finishing with a
   single soft settle-bounce (cloth losing momentum against the
   table) instead of a hard stop. A synced clip-path wipe on the
   *parent* reveals the fabric edge-first, left-to-right, the same
   dye-through-fabric language the atelier notes use for their own
   title/body reveals — so a new piece reads as being smoothed into
   place, not switched on. */
.garment-tilt.is-landing {
  animation: garment-fold-in 0.85s var(--ease-out);
}

.garment.is-landing {
  animation: garment-settle 0.85s var(--ease-out);
}

@keyframes garment-fold-in {
  0%   { clip-path: inset(0 100% 0 0); }
  70%  { clip-path: inset(0 0 0 0); }
  100% { clip-path: inset(0 0 0 0); }
}

@keyframes garment-settle {
  0% {
    opacity: 0;
    transform:
      translate(var(--fit-x, 0%), calc(var(--fit-y, 0%) - 18px))
      scale(calc(var(--fit-scale-x, 1) * 0.94), calc(var(--fit-scale-y, 1) * 1.05))
      skewY(-2.5deg)
      rotate(-4deg);
  }
  45% {
    opacity: 1;
    transform:
      translate(var(--fit-x, 0%), calc(var(--fit-y, 0%) + 2px))
      scale(calc(var(--fit-scale-x, 1) * 1.015), calc(var(--fit-scale-y, 1) * 0.985))
      skewY(0.6deg)
      rotate(1deg);
  }
  72% {
    transform:
      translate(var(--fit-x, 0%), calc(var(--fit-y, 0%) - 1px))
      scale(var(--fit-scale-x, 1), var(--fit-scale-y, 1))
      skewY(-0.2deg)
      rotate(-0.4deg);
  }
  100% {
    opacity: 1;
    transform: translate(var(--fit-x, 0%), var(--fit-y, 0%)) scale(var(--fit-scale-x, 1), var(--fit-scale-y, 1)) skewY(0) rotate(0deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .garment.is-landing,
  .garment-tilt.is-landing { animation: none; }
}

/* ============================================================
   Worn Together mode
   Instead of pretending there's a body, the two flat bays simply
   move into the same column and overlap where a jacket would
   naturally sit over a waistband — the pants bay slides to center
   and shrinks slightly, the jacket bay slides on top of it and up,
   z-index puts the jacket above. Everything that already drives
   scale/tint/fit keeps working unchanged; only position changes.
   ============================================================ */
.table-scene.is-worn .garment-bay--jacket {
  left: 50%;
  width: 40%;
  transform: translateX(-50%) translateY(-15%);
  z-index: 4;
}

.table-scene.is-worn .garment-bay--pants {
  right: 50%;
  width: 40%;
  transform: translateX(50%) translateY(13%);
  z-index: 2;
}

.table-scene.is-worn .garment-bay {
  transition: left 0.65s var(--ease-out), right 0.65s var(--ease-out),
              width 0.65s var(--ease-out), transform 0.65s var(--ease-out);
}

.table-scene:not(.is-worn) .garment-bay {
  transition: left 0.65s var(--ease-out), right 0.65s var(--ease-out),
              width 0.65s var(--ease-out), transform 0.65s var(--ease-out);
}

/* In worn mode the pants sit slightly further from the viewer, and
   its own shadow reads oddly once it's tucked behind the jacket —
   fade it down rather than have two competing shadow pools. */
.table-scene.is-worn .garment-bay--pants .garment-shadow {
  opacity: 0.45;
}

/* The jacket's own resting-on-the-table shadow is sized for the
   flat lay; once the jacket bay overlaps the trouser bay, that
   shadow pool lands on the trouser fabric underneath it and reads
   as a stain. Fade it the same way the trouser's is faded above —
   the per-image drop-shadow filter (tightened for worn mode in
   script.js) already carries enough depth on its own. */
.table-scene.is-worn .garment-bay--jacket .garment-shadow {
  opacity: 0.15;
}

.table-scene.is-worn .garment-bay--jacket .garment-pool {
  opacity: 0.4;
}

.table-scene.is-worn .garment-bay--pants .garment-tilt {
  transform: rotateX(6deg) rotateY(-3deg) translateY(4%);
}

/* Pins read as "pinned flat to a board" — that idea stops making
   sense once the pieces overlap like worn clothing, so they retire
   quietly rather than floating over fabric that isn't flat anymore. */
.table-scene.is-worn .pin {
  opacity: 0;
}

/* Two "bottom of the bay" tags used to sit side by side; once the
   bays overlap into one column, moving the jacket's tag anywhere
   inside its own bay still lands it on top of the trouser fabric
   behind it. The caption line below the table already names both
   pieces together ("bomber worn over a cargo"), so both tags simply
   retire in worn mode rather than compete for a spot that doesn't
   overlap the other garment. */
.table-scene.is-worn .bay-tag {
  opacity: 0;
}

/* The stitched divider marks a seam between two separate flat
   lays; once they're stacked as an outfit there is no seam to
   mark, so it fades out instead of cutting across the composition. */
.table-scene.is-worn .stitch-divider {
  opacity: 0;
}

/* The jacket's own drop-shadow already reads as depth once it's
   sitting over the trouser waist; the pants bay's ambient pool
   would otherwise show through the overlap and look like a stain. */
.table-scene.is-worn .garment-bay--pants .garment-pool {
  opacity: 0.5;
}

@media (prefers-reduced-motion: reduce) {
  .table-scene.is-worn .garment-bay,
  .table-scene:not(.is-worn) .garment-bay {
    transition: none;
  }
}

@media (max-width: 980px) {
  .table-scene.is-worn .garment-bay--jacket {
    width: 58%;
    transform: translateX(-50%) translateY(-7%);
  }
  .table-scene.is-worn .garment-bay--pants {
    width: 42%;
    transform: translateX(50%) translateY(9%);
  }
}

/* Corner pins — a tiny, specific detail that reinforces "pieces
   pinned flat to a board" rather than decoration for its own sake. */
.pin {
  position: absolute;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, var(--brass-bright), var(--brass-dim) 75%);
  box-shadow: 0 1px 2px rgba(0,0,0,0.5);
  z-index: 4;
  opacity: 0.85;
}

.pin--tl { top: 9%; left: 10%; }
.pin--br { bottom: 9%; right: 10%; }

.bay-tag {
  position: absolute;
  bottom: 3%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  margin: 0;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 0.82rem;
  color: var(--ink-faint);
  white-space: nowrap;
}

.stitch-divider {
  position: absolute;
  top: 8%;
  bottom: 8%;
  left: 50%;
  width: 24px;
  transform: translateX(-50%);
  z-index: 3;
  pointer-events: none;
  opacity: 0.35;
  /* Draws open top-to-bottom during the entrance sequence, like the
     dashed seam is being unspooled rather than switched on. Driven
     by --stitch-reveal (0 → 1) from initEntranceSequence(). Defaults
     to 1 so the divider is fully visible if JS never runs. */
  clip-path: inset(0 0 calc((1 - var(--stitch-reveal, 1)) * 100%) 0);
}

.stitch-divider svg {
  width: 100%;
  height: 100%;
}

.stitch-divider path {
  stroke: var(--ink-faint);
  stroke-width: 1;
}

.table-caption {
  margin: 0;
  padding: 0 4px;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 1.05rem;
  color: var(--ink-dim);
}

/* ============================================================
   Control panel
   ============================================================ */
.panel {
  display: flex;
  flex-direction: column;
  min-width: 0;
  transform-origin: 50% 0%;
  transform:
    translateY(calc(var(--drawer-close, 0) * 22px))
    rotateX(calc(var(--drawer-close, 0) * -8deg))
    scale(calc(1 - var(--drawer-close, 0) * 0.03));
  opacity: calc(1 - var(--drawer-close, 0) * 0.55);
  transition: transform 0.1s linear, opacity 0.1s linear;
}

@media (prefers-reduced-motion: reduce) {
  .panel { transform: none; opacity: 1; }
}

.panel__tabs {
  position: relative;
  display: flex;
  border: 1px solid var(--line);
  border-radius: 6px 6px 0 0;
  overflow: hidden;
  background: var(--table);
}

.panel__tab {
  flex: 1;
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 10px;
  justify-content: center;
  padding: 18px 10px;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--ink-faint);
  transition: color 0.3s var(--ease-soft);
}

.panel__tab-glyph {
  width: 18px;
  height: 18px;
}

.panel__tab-glyph svg {
  width: 100%;
  height: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linejoin: round;
}

.panel__tab-label {
  font-size: 0.9rem;
  letter-spacing: 0.01em;
}

.panel__tab[aria-selected="true"] {
  color: var(--room-deep);
}

.panel__tabs-glide {
  position: absolute;
  top: 3px;
  bottom: 3px;
  left: 3px;
  width: calc(50% - 6px);
  background: var(--brass);
  border-radius: 4px;
  z-index: 1;
  transition: transform 0.4s var(--ease-spring);
}

.panel__tab:focus-visible {
  outline: 2px solid var(--brass-bright);
  outline-offset: -3px;
}

.panel__body {
  border: 1px solid var(--line);
  border-top: none;
  border-radius: 0 0 6px 6px;
  background: var(--table);
  padding: 30px 28px 32px;
}

.category {
  animation: panel-in 0.4s var(--ease-out);
}

@keyframes panel-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .category { animation: none; }
}

.category__row {
  padding-bottom: 26px;
  margin-bottom: 26px;
  border-bottom: 1px solid var(--line);
}

.category__row:last-child {
  padding-bottom: 0;
  margin-bottom: 0;
  border-bottom: none;
}

.category__title {
  font-family: var(--font-display);
  font-weight: 460;
  font-style: italic;
  font-size: 1.12rem;
  margin: 0 0 18px;
  color: var(--ink);
}

.category__subtitle {
  font-size: 0.76rem;
  letter-spacing: 0.03em;
  color: var(--ink-faint);
  margin: 0;
  font-weight: 500;
}

.row-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 16px;
}

.row-head__value {
  font-family: var(--font-mono);
  font-size: 0.74rem;
  color: var(--brass-bright);
  white-space: nowrap;
}

.pieces {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.piece-btn {
  width: 78px;
  height: 78px;
  padding: 10px;
  background: var(--room);
  border: 1px solid var(--line);
  border-radius: 4px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  transition: border-color 0.2s var(--ease-soft), transform 0.25s var(--ease-spring), background 0.2s var(--ease-soft);
}

.piece-btn img {
  max-width: 100%;
  max-height: 100%;
  filter: brightness(0.88) contrast(1.05);
  transition: filter 0.2s var(--ease-soft);
}

.piece-btn:hover {
  border-color: var(--brass-dim);
  transform: translateY(-2px);
}

.piece-btn:hover img {
  filter: brightness(1) contrast(1.05);
}

.piece-btn[aria-pressed="true"] {
  border-color: var(--brass);
  background: rgba(201, 146, 79, 0.08);
}

.piece-btn[aria-pressed="true"] img {
  filter: brightness(1.05) contrast(1.05);
}

.piece-btn[aria-pressed="true"]::after {
  content: "";
  position: absolute;
  top: -5px;
  right: -5px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--brass-bright);
  box-shadow: 0 0 8px rgba(232, 178, 111, 0.7);
  animation: pip-in 0.3s var(--ease-spring);
}

@keyframes pip-in {
  from { transform: scale(0); }
  to   { transform: scale(1); }
}

.piece-btn:focus-visible {
  outline: 2px solid var(--brass-bright);
  outline-offset: 2px;
}

/* Tints: small dye pots with a liquid-fill hover swell rather than
   a flat CSS circle — each swatch is what the fabric actually
   simulates to under that filter. */
.tints {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  row-gap: 14px;
}

.tint-btn {
  width: 26px;
  height: 26px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid rgba(244,236,221,0.18);
  cursor: pointer;
  position: relative;
  transition: transform 0.25s var(--ease-spring), border-color 0.2s var(--ease-soft);
  box-shadow: inset 0 1px 2px rgba(0,0,0,0.35), inset 0 -1px 1px rgba(255,255,255,0.12);
}

.tint-btn:hover {
  transform: scale(1.22) translateY(-2px);
}

.tint-btn[aria-pressed="true"] {
  border-color: var(--paper);
  box-shadow: 0 0 0 2px var(--table), 0 0 0 3px var(--brass), inset 0 1px 2px rgba(0,0,0,0.35);
}

.tint-btn:focus-visible {
  outline: 2px solid var(--brass-bright);
  outline-offset: 3px;
}

/* Sizes: read like the little woven size tags stitched into a
   collar or waistband — rectangular, monospaced, no imagery needed
   since the size itself is the whole content. */
.sizes {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.size-btn {
  min-width: 44px;
  padding: 8px 10px;
  background: var(--room);
  border: 1px solid var(--line);
  border-radius: 3px;
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 0.76rem;
  letter-spacing: 0.02em;
  color: var(--ink-dim);
  transition: border-color 0.2s var(--ease-soft), color 0.2s var(--ease-soft),
              background 0.2s var(--ease-soft), transform 0.15s var(--ease-spring);
}

.size-btn:hover {
  border-color: var(--brass-dim);
  color: var(--ink);
  transform: translateY(-1px);
}

.size-btn[aria-pressed="true"] {
  border-color: var(--brass);
  background: rgba(201, 146, 79, 0.12);
  color: var(--brass-bright);
  font-weight: 700;
}

.size-btn:focus-visible {
  outline: 2px solid var(--brass-bright);
  outline-offset: 2px;
}

.size-note {
  margin: 12px 0 0;
  font-size: 0.76rem;
  color: var(--ink-faint);
  font-style: italic;
}

/* Size-match verdict beneath the table — quiet by default, only
   raising color/weight when the two pieces' sizes actually diverge
   enough to be worth flagging. */
.size-match {
  margin: 6px 0 0;
  padding: 0 4px;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.01em;
  color: var(--ink-faint);
}

.size-match[data-tone="match"] {
  color: var(--ink-faint);
}

.size-match[data-tone="close"] {
  color: var(--ink-dim);
}

.size-match[data-tone="gap"] {
  color: var(--brass);
}

/* ============================================================
   Fit & placement controls
   ============================================================ */
.fit-controls {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.fit-control {
  display: grid;
  grid-template-columns: 80px 1fr 42px;
  align-items: center;
  gap: 12px;
}

.fit-control__label {
  font-size: 0.78rem;
  color: var(--ink-dim);
}

.fit-control__value {
  font-size: 0.76rem;
  color: var(--ink-faint);
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-family: var(--font-mono);
}

.fit-control input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 2px;
  background: var(--line);
  border-radius: 2px;
  cursor: pointer;
  margin: 0;
  position: relative;
}

.fit-control input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--brass-bright);
  border: none;
  cursor: pointer;
  box-shadow: 0 0 0 3px var(--table), 0 1px 4px rgba(0,0,0,0.4);
  transition: transform 0.15s var(--ease-spring);
}

.fit-control input[type="range"]:hover::-webkit-slider-thumb,
.fit-control input[type="range"]:active::-webkit-slider-thumb {
  transform: scale(1.3);
}

.fit-control input[type="range"]::-moz-range-thumb {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--brass-bright);
  border: none;
  cursor: pointer;
}

.fit-control input[type="range"]:focus-visible {
  outline: 2px solid var(--brass-bright);
  outline-offset: 3px;
}

/* ============================================================
   Responsive
   ============================================================ */
@media (max-width: 980px) {
  .app {
    padding: 44px 20px 48px;
  }

  .masthead {
    grid-template-columns: 1fr;
    grid-template-areas:
      "id"
      "desc"
      "actions";
  }

  .masthead__kicker {
    white-space: normal;
  }

  .masthead__actions {
    justify-content: flex-start;
  }

  .workspace {
    grid-template-columns: 1fr;
    gap: 36px;
  }

  .table-scene {
    aspect-ratio: 4 / 5;
  }

  .fit-control {
    grid-template-columns: 70px 1fr 38px;
  }

  /* Below the 980px break the panel stacks under the table instead
     of sitting beside it, so "closing like a drawer" tied to the
     table's own exit would fade/tilt the panel before the reader
     has even scrolled to it. The table's own tilt/lamp-dim and the
     workspace recede still apply; only the panel's half of the
     effect stands down here. */
  .panel {
    transform: none;
    opacity: 1;
  }
}

@media (max-width: 560px) {
  .garment-bay--jacket { left: 2%; width: 48%; }
  .garment-bay--pants  { right: 2%; width: 48%; }
  .masthead__seal { width: 56px; height: 56px; }
  .masthead__title { font-size: 1.5rem; }
  .masthead__id { gap: 16px; }
  .masthead__kicker { font-size: 0.62rem; }
}

@media (prefers-reduced-motion: reduce) {
  .garment,
  .knob,
  .panel__tab,
  .panel__tabs-glide,
  .tint-btn,
  .piece-btn,
  .garment-tilt {
    transition: none;
  }
}

/* ============================================================
   Atelier notes — the page's second act.
   Rebuilt around the same idea as the table's scroll tilt/lamp
   effect above: continuous, reversible motion driven straight from
   scroll position (a "scrub"), rather than a flag that fires once
   and then goes inert. initAtelierReveals()/initAtelierSeam() in
   script.js write opacity/transform/clip-path/custom-properties
   directly every frame; the rules below only define the *shape*
   of each effect. Nothing here defaults to opacity:0 — if JS never
   runs, every note is still plain, fully readable, static text.
   ============================================================ */
.atelier {
  position: relative;
  max-width: 720px;
  margin: 108px auto 40px;
  padding-top: 56px;
}

/* Stitched top seam, standing in for a plain border — see
   initAtelierSeam(). A faint static line always sits underneath it
   so the section has a clean edge even before any script runs. */
.atelier__seam {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  z-index: 0;
}

.atelier__seam::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--line);
}

.atelier__seam-thread {
  position: absolute;
  inset: 0;
  transform: scaleX(var(--seam, 0));
  transform-origin: left center;
  background-image: repeating-linear-gradient(90deg, var(--brass) 0 7px, transparent 7px 16px);
  box-shadow: 0 0 6px rgba(232, 178, 111, 0.4);
}

.atelier__seam-needle {
  position: absolute;
  top: 50%;
  left: calc(var(--seam, 0) * 100%);
  width: 7px;
  height: 7px;
  transform: translate(-50%, -50%) rotate(45deg);
  background: linear-gradient(135deg, var(--brass-bright), var(--brass-dim));
  box-shadow: 0 0 8px rgba(232, 178, 111, 0.75);
  opacity: var(--seam-active, 0);
  transition: opacity 0.25s var(--ease-soft);
}

/* Dashed brass thread strung behind the three note numbers, drawn
   downward as the section is read — the same stitch language as
   the table's garment divider, now binding three separate notes
   into one continuous line. Position/height are measured in JS
   (initAtelierThread()) against the actual index badges; only the
   draw-in itself (scaleY) is left to CSS. */
.atelier__thread {
  position: absolute;
  width: 2px;
  transform: scaleY(var(--thread, 0));
  transform-origin: top center;
  background-image: repeating-linear-gradient(180deg, var(--brass) 0 6px, transparent 6px 15px);
  opacity: 0.9;
  z-index: 0;
  pointer-events: none;
}

@media (max-width: 640px) {
  /* The index numbers no longer share a column once the note grid
     stacks, so there is no line to run a thread through. */
  .atelier__thread { display: none; }
}

.atelier__kicker {
  position: relative;
  z-index: 1;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--brass-dim);
  margin: 0 0 48px;
  will-change: opacity, transform;
}

.atelier__note {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 56px 1fr;
  grid-template-areas:
    "index title"
    ".     body";
  column-gap: 24px;
  margin-bottom: 64px;
}

.atelier__note:last-of-type {
  margin-bottom: 0;
}

.atelier__index {
  position: relative;
  grid-area: index;
  font-family: var(--font-mono);
  font-size: 0.9rem;
  color: var(--brass);
  padding-top: 4px;
  display: inline-block;
  will-change: transform, opacity, filter;
}

/* A one-shot brass glint that sweeps across the number the instant
   it finishes landing — kept on a pseudo-element rather than the
   number's own transform/opacity, which script.js is busy driving
   continuously every frame. Triggered by toggling .is-stamped (see
   initAtelierChoreography()); harmless — and invisible — the rest
   of the time. */
.atelier__index::after {
  content: "";
  position: absolute;
  inset: -4px -10px;
  background: linear-gradient(115deg, transparent 30%, rgba(232, 178, 111, 0.6) 50%, transparent 70%);
  background-size: 240% 100%;
  background-position: 150% 0;
  opacity: 0;
  pointer-events: none;
}

.atelier__index.is-stamped::after {
  animation: index-glint 0.75s var(--ease-out);
}

@keyframes index-glint {
  0%   { opacity: 1; background-position: 160% 0; }
  65%  { opacity: 1; }
  100% { opacity: 0; background-position: -60% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .atelier__index.is-stamped::after { animation: none; }
}

.atelier__title {
  grid-area: title;
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 460;
  font-size: 1.5rem;
  color: var(--ink);
  margin: 0 0 14px;
  line-height: 1.25;
  will-change: clip-path, transform, opacity;
}

.atelier__body {
  grid-area: body;
  font-size: 0.95rem;
  line-height: 1.7;
  color: var(--ink-dim);
  margin: 0;
  max-width: 46ch;
  will-change: clip-path, transform, opacity, filter;
}

.atelier__close {
  position: relative;
  z-index: 1;
  margin: 72px 0 0;
  padding-top: 32px;
  font-family: var(--font-mono);
  font-size: 0.76rem;
  letter-spacing: 0.02em;
  color: var(--ink-faint);
  will-change: opacity, transform;
}

/* A second, independent seam for the closing line — same drawn-
   thread idea as the header seam, on its own progress. */
.atelier__close::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  height: 1px;
  width: 100%;
  background: var(--line);
}

.atelier__close::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  height: 1px;
  width: 100%;
  transform: scaleX(var(--close-seam, 0));
  transform-origin: left center;
  background: var(--brass);
  box-shadow: 0 0 6px rgba(232, 178, 111, 0.35);
}

@media (max-width: 640px) {
  .atelier__note {
    grid-template-columns: 1fr;
    grid-template-areas:
      "index"
      "title"
      "body";
    row-gap: 8px;
  }
}
/* ============================================================
   Splash: full-viewport intro shown before the site, every load,
   then removed. Genuine, unbroken black at rest — .splash__blackout
   is an opaque #000 sheet masked with a soft circular hole that
   tracks the lantern, so the walnut table underneath (deliberately
   brightened above its true photographed tone in .splash__wood's own
   filter, since the raw photo is dark enough to read as black even
   when fully "uncovered" — the reveal is spatial, not a brightness
   fade, but the base tone still needs to be legible once revealed)
   is only ever visible exactly where the hole currently is.
   The mask's circle is given an explicit CSS length (--lantern-radius,
   in vmin, set from JS) rather than a bare percentage — a plain
   percentage on a radial-gradient with no explicit size defaults to
   a fraction of the distance to the FARTHEST corner, which is a
   different distance depending on where the beam happens to be on
   screen, so the same "size" value used to read as a noticeably
   bigger or smaller hole depending on position. An explicit length
   makes the beam a fixed, predictable size everywhere. That length
   is written by script.js onto .splash__scene (the shared parent of
   .splash__wood/.splash__blackout/.splash__lantern) rather than onto
   the lantern element itself — custom properties only cascade to an
   element's own descendants, and .splash__blackout is the lantern's
   SIBLING, not its child, so it would never see values set only on
   the lantern.
   The brass seal (.splash__seal-img) is masked the same way, in its
   own local coordinates — not a flat opacity fade — so it is
   uncovered piece by piece as the beam physically passes over it,
   then blended to a guaranteed full reveal once it settles. script.js
   only arms that reveal on the beam's final approach leg, so earlier
   wandering stops never uncover it even if the path happens to pass
   close to center along the way.
   From there: grow (the mark visibly swells a little, a warm flash
   punctuates it) → zoom-dissolve (mark rushes forward, whole splash
   lifts and fades), handing off to the real page. All of it is
   driven frame-by-frame from initSplash() in script.js — the custom
   properties here are just the resting defaults, not animated via
   CSS transition, so they never fight the per-frame values JS sets.
   Respects prefers-reduced-motion: skips straight to removal.
   ============================================================ */
.splash {
  position: fixed;
  inset: 0;
  z-index: 200;
  overflow: hidden;
  background: transparent;
}

.splash.is-transitioning {
  pointer-events: none;
}

.splash__scene {
  position: absolute;
  inset: 0;
}

/* Full-screen table top, brightened well past its true photographed
   tone — a light grade for mood, but never dimmed for concealment,
   and boosted enough that the grain is actually legible once
   uncovered (the raw walnut photo is dark enough on its own that a
   "true-color" reveal reads as barely-lighter black). Concealment is
   entirely .splash__blackout's job (below), so this can stay bright
   and legible the instant the hole passes over any part of it. */
.splash__wood {
  position: absolute;
  inset: -2%;
  background-image: url('assets/images/table/walnut-grain.jpg');
  background-size: cover;
  background-position: center;
  filter: brightness(2.1) contrast(1.12) saturate(0.95) sepia(0.06);
}

/* The actual "screen is genuinely black" layer: solid, opaque #000
   covering the wood completely, with a soft circular hole cut via
   mask-image at the lantern's current position/size. The circle is
   given an explicit size (--lantern-radius, a real length in vmin)
   so "100%" always means that exact radius, not a percentage of the
   distance to whichever corner is farthest away this frame. Fully
   opaque (mask defaults to a 0px radius, which resolves to the
   gradient's last color everywhere) until JS starts positioning it,
   so there's never a frame where anything shows before the light does. */
.splash__blackout {
  position: absolute;
  inset: 0;
  background: #000;
  -webkit-mask-image: radial-gradient(circle var(--lantern-radius, 0px) at var(--lantern-x, 50%) var(--lantern-y, 50%),
      transparent 0%,
      transparent 62%,
      #000 100%);
  mask-image: radial-gradient(circle var(--lantern-radius, 0px) at var(--lantern-x, 50%) var(--lantern-y, 50%),
      transparent 0%,
      transparent 62%,
      #000 100%);
}

/* The lantern's own visible glow — a warm bloom drawn on top of the
   blackout, using screen blend so it reads over both the true-black
   field and the revealed wood alike (soft-light would vanish over
   pure black, since blending anything against 0 stays 0). Sized off
   its own slightly-larger explicit radius so the bloom extends a
   little past the hard-edged hole beneath it. */
.splash__lantern {
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  opacity: var(--lantern-opacity, 0);
  background:
    radial-gradient(circle var(--lantern-glow-radius, 0px) at var(--lantern-x, 50%) var(--lantern-y, 50%),
      rgba(255, 199, 128, 0.5) 0%,
      rgba(232, 164, 96, 0.2) 60%,
      transparent 100%);
}

/* A tighter, hotter core so the center of the beam reads brighter
   than its edge, rather than one flat wash of color. */
.splash__lantern::after {
  content: '';
  position: absolute;
  inset: 0;
  mix-blend-mode: screen;
  opacity: 0.5;
  background:
    radial-gradient(circle var(--lantern-core-radius, 0px) at var(--lantern-x, 50%) var(--lantern-y, 50%),
      rgba(255, 217, 163, 0.6) 0%,
      rgba(201, 146, 79, 0.14) 65%,
      transparent 100%);
}

/* The seal / logo container. Sized and centered here in CSS with a
   formula (min(46vw, 46vh, 380px), dead-center) that script.js's
   initSplash() mirrors exactly in JS to work out where the beam
   currently falls relative to the mark — see .splash__seal-img below
   for the actual masking that keeps it genuinely absent until the
   beam is physically near it, uncovering it piece by piece from
   there rather than fading it in as a flat sheet. */
.splash__seal {
  position: absolute;
  top: 50%;
  left: 50%;
  width: min(46vw, 46vh, 380px);
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%) scale(var(--seal-scale, 0.94));
  will-change: transform;
}

/* The mark itself. Masked spatially by the SAME beam that uncovers
   the wood, translated into this element's own local coordinates
   (--seal-mask-x/y/radius, driven by JS every frame) — so the light
   passing near it genuinely uncovers whichever part it's currently
   over, piece by piece, rather than the whole mark fading in as one
   flat sheet. --seal-mask-radius defaults to 0px, which (like the
   blackout above) resolves to the gradient's last color — fully
   transparent — so the mark is genuinely absent, not a dimmed ghost,
   until JS starts moving the beam near it. --seal-reveal is kept as
   a simple on/off gate (1 once the splash starts; only ever eased
   back down during the final zoom-dissolve), not a fade-in curve. */
.splash__seal-img {
  width: 100%;
  height: 100%;
  display: block;
  position: relative;
  opacity: var(--seal-reveal, 0);
  -webkit-mask-image: radial-gradient(circle var(--seal-mask-radius, 0px) at var(--seal-mask-x, 50%) var(--seal-mask-y, 50%),
      #000 0%,
      #000 55%,
      transparent 100%);
  mask-image: radial-gradient(circle var(--seal-mask-radius, 0px) at var(--seal-mask-x, 50%) var(--seal-mask-y, 50%),
      #000 0%,
      #000 55%,
      transparent 100%);
  filter: drop-shadow(0 0 calc(var(--seal-glow, 0) * 1px) rgba(240, 194, 122, 0.6));
}

.splash.is-seal-lit .splash__seal {
  transition: none;
}

/* Warm flash punctuating the moment the mark is found/at its peak
   grow — a fast pulse layered on top of everything else. */
.splash__flash {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 50% 50%, rgba(240, 194, 122, 0.3) 0%, transparent 58%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.45s var(--ease-soft);
}

.splash.is-flashing .splash__flash {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .splash { display: none; }
}

