/* ════════════════════════════════════════════════════════════════════
   THE STYLESHEET — one story, told in matched frames.

   The two marker blocks below are written by scripts (the brand and the
   fonts). Every other rule uses ONLY those tokens, so re-branding the site
   is a token swap, never a hunt. The only raw colours are pure white and
   black, and only where difference blending needs them.
   ════════════════════════════════════════════════════════════════════ */

/* GENERATED (from the user's brand, written by scripts/brand.py) */
/* P #f2ebe0 = oklch(0.9425 0.0165 79.4) parents surfaces · D #011255 = oklch(0.2316 0.1218 264.0) parents marks · chromatic */
:root{
  --paper:#f2ebe0;
  --paper-2:#e8e1d6;
  --ink:#010621;
  --ink-2:#000112;
  --accent:#1640ad;
  --accent-light:#76a2ff;
  --warm:#b95e00; /* orange 55 */
  --line:rgba(1,18,85,0.16);
  --line-dark:rgba(118,162,255,0.14);
  --ink-dim:rgba(1,6,33,0.55);
  --ink-faint:rgba(1,6,33,0.22);
  --paper-dim:rgba(242,235,224,0.85);
  --shadow:rgba(0,1,18,0.28);
  --shadow-deep:rgba(0,1,18,0.42);
  --row-tint:sepia(1) saturate(1.75) hue-rotate(182deg) brightness(0.42); /* paper text >= 4.61:1 worst case */
  --ink-filter:brightness(8.34) saturate(1.2) drop-shadow(0 0 22px rgba(118,162,255,0.32)); /* line #011255 -> #009bff = 7.04:1 on --ink-2 */
}
/* END GENERATED */
/* FONTS (written at the brand step) */
/* self-hosted (latin + latin-ext, from Google Fonts, SIL Open Font License): no visitor request leaves odyste.com */
@font-face{font-family:'Fauna One';font-style:normal;font-weight:400;font-display:swap;src:url('assets/fonts/fauna-one-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Fauna One';font-style:normal;font-weight:400;font-display:swap;src:url('assets/fonts/fauna-one-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Inter';font-style:normal;font-weight:400;font-display:swap;src:url('assets/fonts/inter-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Inter';font-style:normal;font-weight:400;font-display:swap;src:url('assets/fonts/inter-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Inter';font-style:normal;font-weight:500;font-display:swap;src:url('assets/fonts/inter-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Inter';font-style:normal;font-weight:500;font-display:swap;src:url('assets/fonts/inter-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Outfit';font-style:normal;font-weight:400;font-display:swap;src:url('assets/fonts/outfit-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Outfit';font-style:normal;font-weight:400;font-display:swap;src:url('assets/fonts/outfit-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Outfit';font-style:normal;font-weight:500;font-display:swap;src:url('assets/fonts/outfit-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Outfit';font-style:normal;font-weight:500;font-display:swap;src:url('assets/fonts/outfit-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Outfit';font-style:normal;font-weight:600;font-display:swap;src:url('assets/fonts/outfit-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Outfit';font-style:normal;font-weight:600;font-display:swap;src:url('assets/fonts/outfit-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
@font-face{font-family:'Raleway';font-style:italic;font-weight:500;font-display:swap;src:url('assets/fonts/raleway-italic-latin-ext.woff2') format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF}
@font-face{font-family:'Raleway';font-style:italic;font-weight:500;font-display:swap;src:url('assets/fonts/raleway-italic-latin.woff2') format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
:root{
  --font-display:'Fauna One',Georgia,serif;
  --font-label:'Outfit',system-ui,sans-serif;
  --font-body:'Inter',system-ui,sans-serif;
  --font-em:'Raleway','Fauna One',Georgia,serif;
}
/* END FONTS */

/* layout tokens (not brand): the side gutter never drops under 16px at 375 */
:root{--gut:clamp(16px,2.4vw,36px)}

/* ═══════════════════════════ BASE ═══════════════════════════ */
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:auto;-webkit-text-size-adjust:100%;background:var(--paper)}
html,body{overscroll-behavior:none}
/* the paper the whole site is printed on, explicit: the blank canvas IS this colour.
   overflow-x:clip propagates to the viewport as hidden, so nothing 100vw-wide can ever
   produce a horizontal scroll, without making body a scroll container under the pins */
body{background:var(--paper);color:var(--ink);font-family:var(--font-body);overflow-x:clip;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
body.is-locked{overflow:hidden;height:100vh}
/* while the loader owns the page nothing below the story can take keyboard focus: a Tab into the
   (off-screen) city button scroll-jumped the locked page. Layout and loading are untouched. */
body.is-locked main>:not(.story){visibility:hidden}
img,video{display:block;max-width:none}
a{color:inherit;text-decoration:none}
::selection{background:var(--accent);color:var(--paper)}
em{font-family:var(--font-em);font-style:italic;font-weight:500}

/* THE WAKE MODEL. A mount sleeps with a class, never the [hidden] attribute: any layout
   class (display:flex) outranks the UA's [hidden] rule, and an "empty" canvas that is
   two viewports tall and scrolls is the tell. */
.is-asleep{display:none!important}
/* empty media never renders (separate rules: one unsupported selector would void a whole list) */
img:not([src]),img[src=""]{display:none}
video:not([src]):not(:has(source)){display:none}
/* empty words never take space */
.anno:empty,.caption:empty,.kicker:empty,.sub:empty,.fig:empty,.cue-word:empty,.loader-word:empty,.loader-file:empty,
.title:empty,.craft-title:empty,.life-title:empty,.life-caption:empty,.city-title h2:empty,.cta:empty,.brand:empty,.chip:empty{display:none}

.label{font-family:var(--font-label);font-weight:500;font-size:10.5px;letter-spacing:.24em;text-transform:uppercase;line-height:1.5}

/* keyboard focus must stay visible on every link and button (no border-radius here: at this
   specificity it outranked the city button's pill shape and squared it off on focus) */
a:focus-visible,button:focus-visible{outline:2px solid var(--accent);outline-offset:4px}
[tabindex="-1"]:focus{outline:none}

/* ═══════════════════════════ CHROME ═══════════════════════════ */
.progress{position:fixed;top:0;left:0;right:0;height:2px;z-index:120;pointer-events:none}
.progress i{display:block;height:100%;background:var(--accent);transform-origin:0 50%;transform:scaleX(0)}

/* fixed, difference-blended, and visibility:hidden (not merely transparent) until it is on:
   a keyboard user must never tab into links they cannot see on a locked page */
header.site{
  position:fixed;top:0;left:0;right:0;z-index:110;
  display:flex;justify-content:space-between;align-items:center;gap:24px;
  padding:16px var(--gut);color:var(--ink);
  /* legibility over every frame: a quiet paper bar, not a difference blend (which read brown on
     the sky and vanished on mid-tones). Solid colour, no backdrop-filter: films scrub under it. */
  background:color-mix(in srgb,var(--paper) 90%,transparent);border-bottom:1px solid var(--line);
  opacity:0;visibility:hidden;pointer-events:none;transition:opacity .8s ease,visibility 0s linear .8s;
}
header.site.on{opacity:1;visibility:visible;pointer-events:auto;transition:opacity .8s ease,visibility 0s}
header.site .brand{font-family:var(--font-display);font-size:22px;letter-spacing:.02em;line-height:1;white-space:nowrap}
header.site .nav{display:flex;gap:26px;min-width:0}
header.site .nav a{font-family:var(--font-label);font-weight:600;font-size:10.5px;letter-spacing:.22em;text-transform:uppercase;opacity:.8;transition:opacity .3s;white-space:nowrap}
header.site .nav a:hover,header.site .nav a:focus-visible{opacity:1}
header.site .nav a.keep{opacity:1;color:var(--accent)}
@media(max-width:860px){header.site .nav a:not(.keep){display:none}}
/* the three section links sit in a wrapper that is NOT a box on a desktop (display:contents: the
   links stay flex items of .nav, same gap, same layout); below 780px it becomes the Menu panel */
.nav-sections{display:contents}
.nav-menu{display:none}
/* <picture> wrappers (phone-sized props, the diptych's phone stand-ins) are NOT boxes: each img lays out
   exactly as if it were still a direct child (same containing block, same grid cell in the flat story) */
.pic{display:contents}

/* grain: achromatic noise, multiplied at .05 */
.grain{position:fixed;inset:0;z-index:105;pointer-events:none;opacity:.05;mix-blend-mode:multiply;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}

/* ═══════════════════════════ THE STORY · one pinned stage ═══════════════════════════ */
.story{position:relative;height:100vh;overflow:hidden;background:var(--paper-2);z-index:4}
/* the four stacked layers share ONE identical rect: that is what makes every swap invisible */
.story .layer{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.story .intro{z-index:1}
.story .drawing{z-index:2}
.story .site-still,.story .dive{z-index:3}
.story .finished-still,.story .build{z-index:4}
/* everything the engine fades in starts at 0 */
.story .drawing,.story .site-still,.story .dive,.story .finished-still,.story .build{opacity:0}

/* drafting annotations in the corners */
.anno{position:absolute;z-index:7;max-width:min(38vw,320px);opacity:0;pointer-events:none;color:var(--accent);background:var(--paper);padding:4px 8px;
  font-family:var(--font-label);font-weight:500;font-size:9.5px;letter-spacing:.2em;text-transform:uppercase;line-height:1.5}
.anno::before{content:"";position:absolute;left:-14px;top:.75em;width:8px;height:1px;background:currentColor}
.anno-1{top:96px;left:calc(var(--gut) + 14px)}
.anno-2{top:96px;right:calc(var(--gut) + 14px);text-align:right}
.anno-3{bottom:96px;left:calc(var(--gut) + 14px)}
.anno-4{bottom:120px;right:calc(var(--gut) + 14px);text-align:right}
.anno-2::before,.anno-4::before{left:auto;right:-14px}

/* the desk. Props ship HIDDEN (opacity 0): the entrance sets them visible at the moment
   they fly in, or they would sit on top of the loader film for the whole load.
   --rot is each prop's resting tilt; the engine reads it, so composition lives here. */
.prop{position:absolute;z-index:6;opacity:0;height:auto;pointer-events:none;will-change:transform;
  transform:rotate(var(--rot,0deg));filter:drop-shadow(0 16px 22px var(--shadow))}
.prop-1{--rot:-8deg;top:2vh;right:4vw;width:clamp(120px,19vw,360px)}
.prop-2{--rot:6deg;left:-5vw;bottom:14vh;width:clamp(140px,23vw,430px)}
.prop-3{--rot:12deg;left:16vw;bottom:-9vh;width:clamp(150px,24vw,440px)}
.prop-4{--rot:-14deg;right:2vw;bottom:8vh;width:clamp(120px,17vw,320px)}
@media(max-width:779px){
  .prop-1{width:30vw;top:1vh;right:-4vw}
  .prop-2{width:34vw;left:-10vw;bottom:16vh}
  .prop-3{width:34vw;left:10vw;bottom:-6vh}
  .prop-4{width:28vw;right:-4vw;bottom:10vh}
}

/* the hero: real text over the drawing */
.hero{position:absolute;inset:0;z-index:5;display:flex;flex-direction:column;justify-content:center;align-items:center;
  text-align:center;pointer-events:none;padding:0 var(--gut)}
.hero .kicker{color:var(--paper);background:color-mix(in srgb,var(--ink-2) 86%,transparent);padding:8px 14px;margin-bottom:22px;opacity:0}
/* a soft paper clearing behind the words: the sheet stays visible around it, the line reads on top */
.hero::before{content:"";position:absolute;left:50%;top:50%;width:min(1200px,96vw);height:min(62vh,560px);transform:translate(-50%,-50%);z-index:-1;pointer-events:none;
  background:radial-gradient(closest-side,color-mix(in srgb,var(--paper) 94%,transparent) 55%,color-mix(in srgb,var(--paper) 70%,transparent) 75%,transparent 100%)}
.hero .title{font-family:var(--font-display);font-size:clamp(34px,7vw,122px);line-height:1.04;font-weight:400;color:var(--ink);max-width:100%;overflow-wrap:break-word}
/* until the engine has split it, the line stays invisible: no flash of whole words over the loader */
.hero .title:not(.is-split){opacity:0}
.hero .title em{color:var(--accent)}
/* this drawing is busy behind the words on narrow screens: a soft paper halo keeps every line
   readable without a box (invisible over plain paper) */
.hero .title,.hero .sub{text-shadow:0 0 2px var(--paper),0 0 18px var(--paper),0 0 34px var(--paper-dim)}
/* each word in a clipping span, the inner span 112% below and released on a stagger.
   The padding/margin pair gives descenders and italic overhang room inside the clip. */
.hero .title .w{display:inline-block;overflow:hidden;vertical-align:bottom;padding:0 .05em .12em;margin:0 -.05em -.12em}
.hero .title .w>span{display:inline-block;transform:translateY(112%)}
/* that descender padding eats the 112% offset's margin: an accent or a tall ascender would peek
   out under every word for the whole loader. Each word stays unpainted until its own rise starts
   (opacity, not visibility: a screen reader still reads the line). Flat modes never split. */
.hero .title.is-split .w>span{opacity:0}
.hero .sub{margin-top:26px;color:var(--ink);opacity:0}
.hero .cue{position:absolute;bottom:26px;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;gap:8px;opacity:0}
.cue-line{width:1px;height:44px;background:var(--ink-faint);position:relative;overflow:hidden}
.cue-line i{position:absolute;left:0;top:-40%;width:1px;height:40%;background:var(--ink);animation:cue 1.8s cubic-bezier(.65,0,.35,1) infinite}
@keyframes cue{to{top:120%}}
.cue-word{color:var(--ink-dim)}

/* two lines that speak during the dive: paper-coloured over the film, with a soft ink shadow */
.caption{position:absolute;z-index:5;left:50%;top:14%;font-weight:400;transform:translateX(-50%);width:min(100% - 2*var(--gut),1000px);
  text-align:center;color:var(--paper);font-family:var(--font-display);font-size:clamp(28px,4.6vw,68px);line-height:1.12;
  opacity:0;pointer-events:none;
  text-shadow:0 1px 3px color-mix(in srgb,var(--ink-2) 35%,transparent),0 2px 30px color-mix(in srgb,var(--ink-2) 25%,transparent)}
/* no dive slot (no site still, no dive film): the captions sit on the pale drawing, so they turn ink
   with a soft paper glow, like the city title */
.story--nodive .caption{color:var(--ink);text-shadow:0 1px 3px var(--paper-dim),0 2px 30px color-mix(in srgb,var(--paper) 60%,transparent)}
/* measured on this dive film: caption one speaks over the pale wireframe sheet, caption two over
   the bright studio wall, so both are ink on a soft paper halo */
.caption{color:var(--ink);text-shadow:0 0 2px var(--paper),0 0 16px var(--paper),0 0 34px var(--paper-dim)}
.caption em{color:var(--accent)}

/* the day counter and the stage chip: white + difference, each with its Safari text-shadow */
.day{position:absolute;right:var(--gut);bottom:30px;z-index:5;color:var(--paper);text-align:right;opacity:0;pointer-events:none;
  background:color-mix(in srgb,var(--ink-2) 82%,transparent);padding:14px 18px 12px}
.day-num{font-family:var(--font-display);font-size:clamp(36px,4.6vw,64px);line-height:1;font-variant-numeric:tabular-nums}
.day-of{display:block;color:var(--paper-dim);margin-top:6px}
.phase{position:absolute;left:var(--gut);bottom:30px;z-index:5;color:var(--paper);opacity:0;pointer-events:none}
.chip{display:inline-block;padding:10px 16px;background:color-mix(in srgb,var(--ink-2) 82%,transparent);color:var(--paper)}

/* the loader: percent, bar, file name, over the opening film */
.loader{position:absolute;left:var(--gut);bottom:30px;z-index:8;color:var(--ink);pointer-events:none;max-width:calc(100% - 2*var(--gut));
  background:color-mix(in srgb,var(--paper) 90%,transparent);padding:14px 18px 12px}
.loader-row{display:flex;align-items:baseline;gap:14px}
.loader-pct{font-family:var(--font-display);font-size:40px;line-height:1;font-variant-numeric:tabular-nums}
.loader-word{color:var(--ink)}
.loader-bar{width:210px;max-width:100%;height:1px;background:var(--ink-faint);margin-top:10px}
.loader-bar i{display:block;height:1px;background:var(--ink);transform-origin:0 50%;transform:scaleX(0)}
.loader-file{margin-top:8px;color:var(--ink-dim);min-height:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* ═══════════════════════════ THE DIPTYCH · pinned UNDER the story ═══════════════════════════ */
/* sits under the story (lower z-index) and overlaps its last screen (-100vh), so its own pin
   begins on the exact pixel the story's ends: no dead scroll between them. The body class is
   the one that works while pinned (the pin wraps the story in a spacer, breaking ~). */
.diptych{position:relative;height:100vh;overflow:hidden;background:var(--paper);z-index:2}
.story--pinned ~ .diptych,body.is-story-pinned .diptych{margin-top:-100vh}
.diptych .finished{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:2}
/* the cut-out overlays the finished frame pixel for pixel at rest: same box, same cover mapping */
.cutout-wrap{position:absolute;inset:0;z-index:3;will-change:transform}
/* laid out at the canvas's full cover box (the engine sets --cut-*), so a scaled-down figure is never
   missing the parts the viewport cropped; the .diptych's overflow:hidden crops it at rest */
.cutout-wrap .cutout{position:absolute;left:var(--cut-x,0px);top:var(--cut-y,0px);width:var(--cut-w,100%);height:var(--cut-h,100%);object-fit:cover}
.ink-panel{position:absolute;top:0;bottom:0;left:0;width:50%;background:var(--ink-2);z-index:4;transform:translateX(-101%);overflow:hidden}
/* placed by its measured ink box (the engine sets --ink-upscale, xPercent/yPercent and origin) */
.ink-panel .ink{position:absolute;left:50%;top:50%;width:calc(var(--ink-upscale,1)*100%);height:auto;opacity:0;
  filter:var(--ink-filter);will-change:transform,opacity}
.midline{position:absolute;top:0;bottom:0;left:50%;width:1px;z-index:8;background:var(--ink);transform:scaleY(0);transform-origin:50% 0}
.midline::after{content:"+";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:26px;height:26px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;font-family:var(--font-label);font-size:14px;line-height:1;color:var(--paper);background:var(--ink)}
.fig{position:absolute;bottom:40px;z-index:6;opacity:0}
.fig-a{left:8%;color:var(--accent-light)}
.fig-b{right:8%;color:var(--ink-dim);text-align:right}
/* the two counter-scrolling columns: text up top, the figures stay glorious below */
.belt-col{position:absolute;top:0;bottom:0;width:50%;overflow:hidden;z-index:5;opacity:0;pointer-events:none}
.belt-col-l{left:0}
.belt-col-r{right:0}
.belt{position:absolute;left:0;right:0;top:0;will-change:transform}
.cell{height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;padding:13vh 6vw 0;gap:16px;text-align:center}
.cell h2{font-family:var(--font-display);font-weight:400;font-size:clamp(24px,3vw,48px);line-height:1.14}
.belt-col-l .cell{color:var(--paper);text-shadow:0 0 2px var(--ink-2),0 0 14px var(--ink-2),0 0 30px var(--ink-2)}
.belt-col-l .cell .label,.belt-col-l .cell h2 em{color:var(--accent-light)}
.belt-col-r .cell{color:var(--ink);text-shadow:0 0 2px var(--paper),0 0 14px var(--paper),0 0 30px var(--paper)}
.belt-col-r .cell .label{color:var(--ink-dim)}
.belt-col-r .cell h2 em{color:var(--warm)}

/* ═══════════════════════════ PAIRS, STACKED (under 780px; static and reduced too) ═══════════════════════════ */
.pairs-mobile{display:none;background:var(--paper)}
.pair-card{display:grid;grid-template-columns:1fr 1fr;min-height:70vh}
.pair-card .half{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:12px;text-align:center;padding:40px max(var(--gut),6vw);min-width:0}
.pair-card .half h2{font-family:var(--font-display);font-weight:400;font-size:clamp(24px,3vw,44px);line-height:1.14;overflow-wrap:break-word}
.pair-card .half .label{color:var(--ink-dim)}
.pair-card .half h2 em{color:var(--warm)}
.pair-card .half.dark{background:var(--ink-2);color:var(--paper)}
.pair-card .half.dark .label,.pair-card .half.dark h2 em{color:var(--accent-light)}
/* M9: below DESKTOP_MIN, and on a phone turned to landscape (PHONE_LAND), CSS swaps the belts for
   the cards. The query list MUST equal the one script.js diptych() skips its pin under (DESKTOP_MIN
   and PHONE_LAND): a pin on a display:none section breaks every scroll length after it. */
@media (max-width:779px), (hover:none) and (pointer:coarse) and (orientation:landscape) and (max-height:500px){
  .diptych{display:none}
  .pairs-mobile:not(.is-asleep){display:block}
}
@media(max-width:779px){
  .pair-card{grid-template-columns:none;grid-template-rows:1fr 1fr}
  .pair-card .half h2{font-size:clamp(24px,6.4vw,38px)}
}
body.static .diptych,body.reduced .diptych{display:none}
body.static .pairs-mobile:not(.is-asleep),body.reduced .pairs-mobile:not(.is-asleep){display:block}

/* ═══════════════════════════ THE CRAFT · rows and numbers ═══════════════════════════ */
.craft{background:var(--paper);padding:16vh max(var(--gut),6vw) 10vh;position:relative}
.craft .kicker{color:var(--accent);margin-bottom:10px}
.craft-title{font-family:var(--font-display);font-size:clamp(34px,4.6vw,72px);font-weight:400;line-height:1.08;margin-bottom:9vh;overflow-wrap:break-word}
.craft-title em{color:var(--accent)}
.row{display:grid;grid-template-columns:80px minmax(0,1fr) minmax(0,1.1fr) auto;gap:3vw;align-items:baseline;
  padding:34px 18px;border-top:1px solid var(--line);position:relative;overflow:hidden;cursor:default}
.row:last-child{border-bottom:1px solid var(--line)}
/* on hover the ENTIRE row becomes its photograph, tinted to the brand and darkened so paper
   text on top clears 4.5:1. Without a photo the frame is the deep ink. */
.row-photo{position:absolute;inset:0;z-index:0;opacity:0;pointer-events:none;background-color:var(--ink-2);
  background-size:cover;background-position:center 35%;filter:var(--row-tint);transition:opacity .55s cubic-bezier(.65,0,.35,1)}
.row>*:not(.row-photo){position:relative;z-index:1;transition:color .4s ease}
.row-num{color:var(--accent)}
.row-name{font-family:var(--font-display);font-size:clamp(24px,2.7vw,40px);line-height:1.05;overflow-wrap:break-word}
.row-desc{font-family:var(--font-body);font-size:13px;line-height:1.75;color:var(--ink-dim);overflow-wrap:break-word}
.row-arrow{font-family:var(--font-label);font-size:14px;color:var(--accent);transform:translateX(-8px);opacity:0;transition:transform .4s,opacity .4s,color .4s}
/* .is-hover is set by the engine (survives scrolling); native :hover only where no engine runs */
.row.is-hover .row-photo,body.static .row:hover .row-photo{opacity:1}
.row.is-hover .row-num,.row.is-hover .row-name,.row.is-hover .row-desc,
body.static .row:hover .row-num,body.static .row:hover .row-name,body.static .row:hover .row-desc{color:var(--paper)}
.row.is-hover .row-arrow,body.static .row:hover .row-arrow{transform:none;opacity:1;color:var(--paper)}
@media(max-width:860px){
  .row{grid-template-columns:44px minmax(0,1fr);padding:28px 8px}
  .row-desc{grid-column:2}
  .row-arrow{display:none}
}
.numbers{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);gap:2vw;margin-top:12vh}
@media(max-width:860px){.numbers{grid-auto-flow:row;grid-template-columns:repeat(2,minmax(0,1fr));gap:6vw}}
.num-v{font-family:var(--font-display);font-size:clamp(38px,5vw,80px);line-height:1;color:var(--ink);font-variant-numeric:tabular-nums;overflow-wrap:break-word}
.num-v i{font-style:normal}
.num-l{margin-top:10px;color:var(--ink-dim)}

/* ═══════════════════════════ THE LIFE · a window cut in the page ═══════════════════════════ */
.life{background:var(--paper);position:relative;padding:10vh 0 12vh;text-align:center;overflow-x:clip}
.life .kicker{color:var(--accent);display:block;padding:0 var(--gut)}
.life-title{font-family:var(--font-display);font-size:clamp(30px,4.2vw,64px);font-weight:400;line-height:1.18;margin:16px auto 7vh;width:min(100% - 2*var(--gut),940px);overflow-wrap:break-word}
.life-title em{color:var(--warm)}
/* FULL width means full width: no gutter, no max-width, 100vw. A parallax window with paper
   down both sides reads as a mistake. The engine sets the clip-path from WINDOW_SHAPE. */
.window{position:relative;width:100vw;max-width:none;margin:0;height:min(92vh,900px);overflow:hidden}
/* oversized by --family-over (the engine sets it from FAMILY_OVER) so it can travel behind the hole */
.life-img{position:absolute;left:0;top:0;width:100%;height:calc(var(--family-over,1.35)*100%);object-fit:cover;will-change:transform}
.life-caption{margin-top:5vh;color:var(--ink-dim);padding:0 var(--gut)}
@media(max-width:779px){.window{height:120vw}}

/* ═══════════════════════════ THE CITY · the last screen ═══════════════════════════ */
.city{position:relative;height:100vh;overflow:hidden;background:var(--paper)}
.city .back,.city .back-still{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:1}
/* the film wins over the stills-mode still, as everywhere in the story: the masked front film must
   grow over ITS OWN frame zero (shared sky, no edge), and the still stays underneath as a fallback */
.city .back{z-index:2}
/* the front copy grows ONLY inside its own silhouette: mask defaults here, the image is set
   inline by the engine from data-mask. No wipe, no inset() clip, ever: that drew a hard line. */
.city .front{position:absolute;inset:0;z-index:4;pointer-events:none;
  -webkit-mask-size:cover;mask-size:cover;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}
.city .front-film,.city .front-still{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.city .front-still{opacity:0}
/* the title and the button sit ABOVE both films at all times, in ink, with a soft paper glow.
   No backdrop-filter: the film under it changes every frame and a live blur would re-rasterise. */
.city-title{position:absolute;inset:0;z-index:5;isolation:isolate;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:22px;
  text-align:center;pointer-events:none;padding:0 var(--gut)}
.city-title::before{content:"";position:absolute;left:50%;top:50%;width:min(1250px,100vw);height:min(70vh,640px);transform:translate(-50%,-50%);z-index:-1;pointer-events:none;
  background:radial-gradient(closest-side,color-mix(in srgb,var(--paper) 93%,transparent) 50%,color-mix(in srgb,var(--paper) 66%,transparent) 74%,transparent 100%)}
.city-title .kicker{color:var(--paper);background:color-mix(in srgb,var(--ink-2) 72%,transparent);padding:8px 16px}
.city-title h2{font-family:var(--font-display);font-size:clamp(46px,8.6vw,150px);font-weight:400;line-height:1;color:var(--ink);max-width:100%;overflow-wrap:break-word;
  text-shadow:0 1px 4px var(--paper-dim),0 2px 34px color-mix(in srgb,var(--paper) 60%,transparent)}
.city-title h2 em{color:var(--accent)}
.cta{pointer-events:auto;margin-top:16px;display:inline-block;max-width:100%;border:1.5px solid var(--ink);color:var(--ink);padding:20px 44px;border-radius:999px;
  font-family:var(--font-label);font-weight:600;font-size:12px;letter-spacing:.3em;text-transform:uppercase;
  background:color-mix(in srgb,var(--paper) 62%,transparent);transition:background .35s,color .35s,transform .35s}
.cta:hover,.cta:focus-visible{background:var(--ink);color:var(--paper);transform:translateY(-2px)}
@media(max-width:779px){.cta{padding:18px 28px;letter-spacing:.22em}}

/* ═══════════════════════════ THE PROJECT FORM · a sheet laid over the page ═══════════════════════════
   A native <dialog>: the browser traps focus, Esc closes it and the page behind is inert. It
   scrolls on its own (data-lenis-prevent lets the wheel through while Lenis is stopped) and
   overscroll never chains into the page. Drafting-paper grid, same tokens as everything else. */
.brief{position:fixed;inset:0;margin:auto;width:min(780px,100% - 2*var(--gut));height:auto;max-height:min(92vh,1000px);
  padding:0;border:0;background:var(--paper);color:var(--ink);overflow:auto;overscroll-behavior:contain;
  box-shadow:0 40px 120px var(--shadow-deep)}
.brief::backdrop{background:color-mix(in srgb,var(--ink-2) 64%,transparent);backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px)}
.brief[open]{animation:brief-in .5s cubic-bezier(.2,.7,.2,1)}
@keyframes brief-in{from{opacity:0;transform:translateY(28px)}}
.brief-form,.brief-done{position:relative;padding:clamp(26px,5vw,58px);display:flex;flex-direction:column;gap:28px;
  background-image:linear-gradient(color-mix(in srgb,var(--accent) 7%,transparent) 1px,transparent 1px),
    linear-gradient(90deg,color-mix(in srgb,var(--accent) 7%,transparent) 1px,transparent 1px);
  background-size:32px 32px}
.brief-done{display:none;min-height:min(60vh,520px);justify-content:center}
.brief.is-sent .brief-form{display:none}
.brief.is-sent .brief-done{display:flex}
.brief-head{display:flex;flex-direction:column;gap:14px;padding-right:48px}
.brief .kicker{color:var(--accent)}
.brief h2{font-family:var(--font-display);font-weight:400;font-size:clamp(30px,4.4vw,54px);line-height:1.08}
.brief h2 em{color:var(--accent)}
.brief-intro{font-family:var(--font-body);font-size:15px;line-height:1.65;color:var(--ink-dim);max-width:54ch}
.brief-close{position:absolute;top:clamp(16px,3vw,28px);right:clamp(16px,3vw,28px);z-index:2;width:44px;height:44px;border-radius:50%;
  border:1.5px solid var(--ink-faint);background:var(--paper);color:var(--ink);font:300 26px/1 var(--font-body);cursor:pointer;
  transition:background .3s,color .3s,border-color .3s}
.brief-close:hover{background:var(--ink);color:var(--paper);border-color:var(--ink)}
.brief-group{border:0;min-width:0;display:flex;flex-direction:column;gap:20px}
.brief-group legend{color:var(--accent);margin-bottom:8px;padding:0}
.brief-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px 28px}
.field{display:flex;flex-direction:column;gap:8px;min-width:0}
.field .label{color:var(--ink-dim)}
.field input,.field select,.field textarea{width:100%;font:400 16px/1.5 var(--font-body);color:var(--ink);background:transparent;
  border:0;border-bottom:1.5px solid var(--ink-faint);border-radius:0;padding:8px 0;outline:none;transition:border-color .25s}
.field textarea{resize:vertical;min-height:130px;border:1.5px solid var(--ink-faint);padding:12px 14px;background:color-mix(in srgb,var(--paper) 70%,transparent)}
.field select{appearance:none;-webkit-appearance:none;cursor:pointer;padding-right:26px;
  background:linear-gradient(45deg,transparent 50%,var(--ink) 50%) calc(100% - 9px) 55%/6px 6px no-repeat,
             linear-gradient(-45deg,transparent 50%,var(--ink) 50%) calc(100% - 4px) 55%/6px 6px no-repeat}
.field ::placeholder{color:var(--ink-faint)}
.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--accent)}
/* only after a send attempt: never shout at someone who has not typed yet */
.brief-form.was-tried :invalid{border-color:var(--warm)}
.brief-form.was-tried .consent input:invalid+span{color:var(--warm)}
.chips{display:flex;flex-wrap:wrap;gap:10px}
.chip-opt{position:relative}
.chip-opt input{position:absolute;opacity:0;width:1px;height:1px}
.chip-opt span{display:inline-block;padding:11px 18px;border:1.5px solid var(--ink-faint);border-radius:999px;cursor:pointer;
  font:500 11.5px/1 var(--font-label);letter-spacing:.16em;text-transform:uppercase;color:var(--ink);background:var(--paper);
  transition:background .25s,color .25s,border-color .25s}
.chip-opt span:hover{border-color:var(--ink)}
.chip-opt input:checked+span{background:var(--ink);color:var(--paper);border-color:var(--ink)}
.chip-opt input:focus-visible+span{outline:2px solid var(--accent);outline-offset:3px}
.consent{display:flex;gap:12px;align-items:flex-start;font:400 13px/1.55 var(--font-body);color:var(--ink-dim);cursor:pointer}
.consent input{margin-top:3px;width:16px;height:16px;flex:none;accent-color:var(--accent)}
/* the honeypot: real people never see it, form-filling bots do */
.brief-trap{position:absolute;left:-9999px;width:1px;height:1px;opacity:0}
.brief-status{min-height:1.5em;font:400 14px/1.5 var(--font-body);color:var(--accent)}
.brief-status a{text-decoration:underline}
.brief .cta{margin-top:0;align-self:flex-start;cursor:pointer;background:var(--paper)}
.brief .cta[disabled]{opacity:.5;cursor:progress}
/* full-screen on a narrow screen AND on a phone turned to landscape (390px tall: a floating sheet
   left 360px for the form). The × rides along (sticky) so it never scrolls out of reach. */
@media (max-width:600px), (pointer:coarse) and (max-height:500px){
  .brief{width:100%;max-width:100%;height:100%;max-height:100%}
  .brief-grid{grid-template-columns:1fr}
  .brief .cta{align-self:stretch;text-align:center}
  .brief-close{position:sticky;top:12px;right:auto;align-self:flex-end;margin-bottom:-72px}
  /* the thank-you fills the sheet; the extra top padding (twice the text + button below the line) puts
     "Merci" itself, not the whole block, at the middle of the screen */
  .brief-done{min-height:100%;padding-top:calc(clamp(26px,5vw,58px) + min(96px, 20vh))}
}
@media(prefers-reduced-motion:reduce){.brief[open]{animation:none}}

/* ═══════════════════════════ STATIC + REDUCED · the flat story ═══════════════════════════
   No pins, no scrubbing: the story becomes a stack. Panel one is the drawing with the hero
   copy; panel two (when present) is the finished frame carrying the captions as text.
   Absolute children of a grid use their grid AREA as containing block, so the same
   layered markup lays out as two stacked screens without moving a single node. */
.story--flat{height:auto;display:grid;grid-template-columns:100%;grid-template-rows:100vh}
.story--flat.story--two{grid-template-rows:100vh 100vh}
/* explicit end lines: for an absolutely positioned grid child an `auto` end line is the
   container's padding edge, so a bare `grid-row:1` would stretch panel one over both screens */
.story--flat>*,.story--flat>.pic>*{grid-column:1/2;grid-row:1/2}
.story--flat.story--two>.flat-end,.story--flat.story--two>.caption,.story--flat.story--two>.day,.story--flat.story--two>.phase{grid-row:2/3}
/* panel two sits above anything of panel one that overhangs it (a prop parked past the edge) */
.story--flat.story--two>.flat-end{z-index:7}
.story--flat.story--two>.caption,.story--flat.story--two>.day,.story--flat.story--two>.phase{z-index:8}
.story--flat .layer{display:none}
.story--flat .layer.flat-start,.story--flat .layer.flat-end{display:block;opacity:1}
.story--flat .loader{display:none}
.story--flat .hero .title,.story--flat .hero .kicker,.story--flat .hero .sub,.story--flat .hero .cue,
.story--flat .anno,.story--flat .prop,.story--flat .caption,.story--flat .day,.story--flat .phase{opacity:1}
.story--flat .caption-2{top:auto;bottom:14%}
/* nothing below the hero yet: a scroll cue on a page that cannot scroll reads as broken */
.story--nocue .hero .cue{display:none}
.story--flat.story--noend .caption{color:var(--ink);text-shadow:none}
body.static header.site,body.reduced header.site{transition:none}
body.static .progress,body.reduced .progress{display:none}

/* ═══════════════════════════ REDUCED MOTION ═══════════════════════════ */
@media(prefers-reduced-motion:reduce){
  .cue-line i{animation:none;top:30%}
  .row-photo,.row>*:not(.row-photo),.row-arrow,.cta,header.site{transition:none}
}

/* ═══════════════════════════ THE FOOT · © and the legal links ═══════════════════════════
   Append at the very END of style.css (after REDUCED MOTION). Colours, type and spacing use ONLY
   the site's tokens (--paper, --ink, --accent, --line, --gut, --font-*), plus --ink-soft defined
   just below. legal.css carries the same rules for the two legal pages.
   Ink on paper ≈ 16.9:1, accent on paper ≈ 7.5:1, --ink-soft on paper ≈ 7.8:1. */
/* ink thinned on paper: secondary text that still clears 4.5:1 (--ink-dim does not, ≈ 4.3:1).
   Same value as --ink-soft in legal.css. */
:root{--ink-soft:color-mix(in srgb,var(--ink) 72%,var(--paper))}
.site-foot{position:relative;z-index:1;display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:4px 32px;
  padding:22px var(--gut) 26px;background:var(--paper);color:var(--ink);border-top:1px solid var(--line);
  font-family:var(--font-label);font-weight:500;font-size:10.5px;letter-spacing:.22em;text-transform:uppercase;line-height:1.5}
.site-foot p{margin:0;padding:12px 0}
.foot-nav{display:flex;flex-wrap:wrap;column-gap:26px}
/* 12px of padding above and below: each link is a 40px-tall target */
.foot-nav a{display:inline-block;padding:12px 0;font-weight:600;color:var(--ink);text-decoration:none;transition:color .25s}
.foot-nav a:hover,.foot-nav a:focus-visible,.foot-nav a[aria-current="page"]{color:var(--accent)}
.foot-nav a:focus-visible{outline:2px solid var(--accent);outline-offset:4px}
@media(max-width:600px){.site-foot{flex-direction:column;align-items:flex-start}}
/* while the loader owns the page, nothing below the story may take keyboard focus
   (same reason as `body.is-locked main>:not(.story)` in BASE: a Tab scroll-jumped the locked page) */
body.is-locked .site-foot{visibility:hidden}
@media(prefers-reduced-motion:reduce){.foot-nav a{transition:none}}

/* ═══════════════════════════ THE FORM NOTICE (see form-notice.html) ═══════════════════════════
   The consent line now carries a link, and a short sentence sits under the send button.
   Global `a{color:inherit;text-decoration:none}` would hide the link, so it is restyled here.
   The consent text moves from --ink-dim (≈ 4.3:1 on paper, under 4.5:1 for 13px text) to --ink-soft.
   `.brief .consent` (0,2,0) outranks the `.consent` rule in THE PROJECT FORM wherever this block is
   pasted; the was-tried warning colour on the span still wins (it is more specific). */
.brief .consent{color:var(--ink-soft)}
.consent a,.brief-note a{color:var(--accent);text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:.18em}
.consent a:hover,.brief-note a:hover{text-decoration-thickness:2px}
.brief-note{margin-top:-14px;max-width:62ch;font:400 12.5px/1.6 var(--font-body);color:var(--ink-soft)}

/* ═══════════════════════════ PHONES · "la planche" (BUILD-LOG "ODYSTE v3 · mobile") ═══════════════════════════
   Everything below is scoped to a phone query: a desktop at 780px and above with a mouse matches none of it.
   The four queries are written identically here, in script.js and in index.html:
     PHONE       (max-width:779px), PHONE_LAND
     PHONE_LAND  (hover:none) and (pointer:coarse) and (orientation:landscape) and (max-height:500px)   = script.js PHONE_LAND
     STAGE       (max-width:779px) and (orientation:portrait)       = script.js STAGE_QUERY = the phone films' <source media>
     CITY_PHONE  (max-width:779px) and (max-aspect-ratio:7/8)       = the city phone film's <source media> */

/* ── PHONE: header pills, labels, pair cards with their figures, craft, form, footer ── */
@media (max-width:779px), (hover:none) and (pointer:coarse) and (orientation:landscape) and (max-height:500px){
  header.site{height:60px;padding-top:0;padding-bottom:0}
  /* a 44px target for the brand, without moving its letters */
  header.site .brand{display:inline-flex;align-items:center;min-height:44px}
  header.site .nav{align-items:center;gap:10px}
  /* Contact becomes the accent pill; the Menu pill (shown below 780px only, see the next block) is its ink twin */
  header.site .nav a.keep,.nav-menu{align-items:center;min-height:44px;padding:0 18px;border:1.5px solid var(--accent);border-radius:999px;line-height:1}
  header.site .nav a.keep{display:inline-flex}
  .nav-menu{border-color:var(--ink);color:var(--ink);background:transparent;cursor:pointer;
    font-family:var(--font-label);font-weight:600;font-size:10.5px;letter-spacing:.22em;text-transform:uppercase}

  .label{font-size:11.5px;letter-spacing:.2em}
  .loader-word{font-size:10.5px;letter-spacing:.14em;white-space:nowrap}
  /* it showed raw file paths ("assets/dive-phone.mp4") to a reader who cannot do anything with them */
  .loader-file{display:none}
  .loader-pct{font-size:34px}
  /* secondary text that clears 4.5:1 on paper (--ink-dim does not) */
  .num-l,.life-caption,.field .label,.brief-intro{color:var(--ink-soft)}

  /* "le plan / le produit" on every card: the glowing ink plan above the promise, the cut-out product
     under the answer. Both figures are crops of the SAME measured ink boxes (scripts/figs.py), so a
     detail framed by --fz/--fx/--fy lands on the same part of the monitor or phone in both halves. */
  .pair-card{min-height:0}
  .pair-card .half{position:relative;justify-content:flex-start;gap:10px;padding:30px var(--gut) 28px}
  .pair-card .half.dark::before,.pair-card .half:not(.dark)::after{content:"";display:block;flex:none;width:min(66%,300px);aspect-ratio:1.66;
    background-repeat:no-repeat;background-size:calc(var(--fz,1) * 100%) auto;background-position:var(--fx,50%) var(--fy,50%)}
  .pair-card .half.dark::before{background-image:url("assets/fig-plan.webp");filter:var(--ink-filter);margin-bottom:4px}
  .pair-card .half:not(.dark)::after{background-image:url("assets/fig-product.webp");margin-top:4px}
  /* card 1 shows both whole figures; cards 2-4 the matching detail */
  .pair-card:nth-child(2){--fz:2;--fx:30%;--fy:14%}
  .pair-card:nth-child(3){--fz:1.9;--fx:100%;--fy:84%}
  .pair-card:nth-child(4){--fz:2;--fx:50%;--fy:80%}
  /* the desktop midline's "+" disc, on the seam between the two halves */
  .pair-card .half:not(.dark)::before{content:"+";position:absolute;top:0;left:50%;z-index:1;transform:translate(-50%,-50%);width:26px;height:26px;border-radius:50%;
    display:flex;align-items:center;justify-content:center;font-family:var(--font-label);font-size:14px;line-height:1;color:var(--paper);background:var(--ink)}
  .pair-card .half:not(.dark) .label{color:var(--ink-soft)}
  /* every line reserves two rows, so the four cards share one height (about 600px at 390) and one rhythm */
  .pair-card .half h2{min-height:2.28em}

  .row-desc{font-size:15px;line-height:1.6;color:var(--ink-soft)}
  .chip-opt span{padding:16px 18px}
  .foot-nav a{padding:14px 0;min-height:44px}
}

/* ── the Menu panel: below 780px only (780-860 keeps Contact alone, as before) ── */
@media (max-width:779px){
  .nav-menu{display:inline-flex}
  .nav-sections{display:none}
  header.site.is-menu .nav-sections{display:flex;flex-direction:column;position:absolute;top:100%;left:0;right:0;
    padding:6px var(--gut) 10px;background:var(--paper);border-bottom:1px solid var(--line)}
  /* ties `header.site .nav a:not(.keep)` (0,3,2) in the 860px rule above and wins by coming later */
  header.site.is-menu .nav-sections a{display:block;padding:14px 0;min-height:44px;font-size:12px;opacity:1}
}

/* ── THE PORTRAIT STAGE (STAGE): every story layer in ONE box ──
   Measured on the frames (PIL): the drawing's ink spans x .230-.820 and y .029-.959 (it needs the full
   height); the build's last frame (= cutout.webp alpha) spans x .262-.772 (monitor .262-.671, phone
   .688-.772). A window 2/3 of the frame wide at object-position 56% covers x .1867-.8533: the drawing
   keeps 25/20px of margin at 390, the build 44/48px, the coffee stain (x > .89) stays out.
   Box: --box-w wide (100vw unless the screen is short), aspect 960/814 (= 2/3 x 1440/814), the picture
   cover-fitted by its height (no vertical crop). The phone films ARE that window (scripts/frames.py
   encode-phone --window stage: crop 960:814 at x 269 of the 1440 scale), so they fill the box exactly
   and a desktop film shown here (a browser that ignores <source media>) shows the same pixels.
   Same query as STAGE_QUERY in script.js and the <source media> of the phone films.
   The engine writes --zoom on each layer (the push-in, PUSH_AT -> BUILD_IN); the matching inset()
   keeps the scaled picture inside the box, so all four layers stay one identical rect. The push-in
   origin: y 56.2% = CUTOUT_INK's centre; x 47.4% = the point that splits the margin EVENLY between the
   monitor's left edge (CUTOUT_INK l .262 -> .1129 of the window) and the phone's right edge (r .7724
   -> .8785) at 1.2: .0407 x the box each side (16px at 390, 13px at 320), where the cut-out centre
   (49.6%) left the monitor 11.6px at 320. inset() = origin x (1 - 1/zoom) on each side. */
@media (max-width:779px) and (orientation:portrait){
  .story{
    --hdr:60px;
    --vis:100vh;
    --title-fs:clamp(38px, min(12.5vw, calc(var(--vis) * .072)), 60px);
    --zone-top:calc(64px + 2.2 * var(--title-fs));
    /* the bottom zone the box always leaves free: sub + cue (102px), or 18px + the day plate (97px) */
    --below:104px;
    --box-w:min(100vw, calc((var(--vis) - var(--hdr) - var(--zone-top) - var(--below)) * 960 / 814));
    --box-h:calc(var(--box-w) * 814 / 960);
    --box-top:calc(var(--hdr) + max(var(--zone-top), min((var(--vis) - var(--hdr) - var(--box-h)) * .62, var(--vis) - var(--hdr) - var(--box-h) - var(--below))));
  }
  .story .layer{inset:auto;top:var(--box-top);left:calc(50% - var(--box-w) / 2);width:var(--box-w);height:var(--box-h);
    object-fit:cover;object-position:56% 50%;
    transform:scale(var(--zoom,1));transform-origin:47.4% 56.2%;
    -webkit-clip-path:inset(calc(56.2% * (1 - 1 / var(--zoom,1))) calc(52.6% * (1 - 1 / var(--zoom,1))) calc(43.8% * (1 - 1 / var(--zoom,1))) calc(47.4% * (1 - 1 / var(--zoom,1))));
    clip-path:inset(calc(56.2% * (1 - 1 / var(--zoom,1))) calc(52.6% * (1 - 1 / var(--zoom,1))) calc(43.8% * (1 - 1 / var(--zoom,1))) calc(47.4% * (1 - 1 / var(--zoom,1))))}
  /* the hairline frame: under layers 1-4, so only its outer pixel shows */
  .story::before{content:"";position:absolute;z-index:0;pointer-events:none;top:calc(var(--box-top) - 1px);left:calc(50% - var(--box-w) / 2 - 1px);
    width:calc(var(--box-w) + 2px);height:calc(var(--box-h) + 2px);border:1px solid var(--line)}

  /* the hero: kicker and title on the paper above the box, the sub and the cue below it */
  .hero{inset:auto;top:var(--hdr);left:0;right:0;height:calc(var(--box-top) - var(--hdr) - 14px)}
  .hero::before{display:none}
  /* the exit fades in place: its 16vh drift carried the sub up into the picture and the title under the header */
  .story .hero{transform:none!important}
  .hero .kicker{margin-bottom:14px}
  /* 5.2em: "De l'encre" is 4.7em wide in Fauna One, so the line breaks after it, and the title's box stays
     narrow enough that the corner props never reach it */
  .hero .title{font-size:var(--title-fs);line-height:1.02;max-width:5.2em;text-wrap:balance}
  /* two short centred lines (each about 16.5em): narrow (and never wider than .74 of the box, which the
     bottom props leave free), so the corner props stay clear of it */
  .hero .sub{position:absolute;left:50%;top:calc(100% + 14px + var(--box-h) + 22px);width:min(calc(100% - 2 * var(--gut)), 18.5em, calc(var(--box-w) * .74));margin:0;transform:translateX(-50%);text-wrap:balance}
  .hero .cue{top:calc(100% + 14px + var(--box-h) + 66px);bottom:auto}
  /* 36px, shortened on a short screen so the cue still ends inside the visible zone */
  .cue-line{height:clamp(12px, calc(var(--vis) - var(--box-top) - var(--box-h) - 70px), 36px)}
  /* the sub already says "Faites défiler" */
  .hero .cue-word{display:none}

  /* decorative tags: four tags and four props do not fit at 390 */
  .anno{display:none}
  /* the props sit on the four box corners, partly off-screen, as they sit on the drawing on a desktop.
     Sized and placed in box widths (B = --box-w; B = 100vw on every phone that is not short): each prop's
     ROTATED bounding box reaches exactly .12B into the box on both axes (at most 12vw x 12vw of a corner,
     + 7px of idle drift), so a corner prop can never reach the title (5.2em wide) or the sub (18.5em) on
     any phone. left/top = the box corner -/+ .12B - the rotated half-extent w(1 ± (|cos|+|sin|))/2. */
  .prop{right:auto;bottom:auto}
  .prop-1{width:calc(var(--box-w) * .24);top:calc(var(--box-top) - var(--box-w) * .1355);left:calc(50% + var(--box-w) * .3955)}
  .prop-4{width:calc(var(--box-w) * .26);top:calc(var(--box-top) - var(--box-w) * .1676);left:calc(50% - var(--box-w) * .6676)}
  .prop-2{width:calc(var(--box-w) * .28);top:calc(var(--box-top) + var(--box-h) - var(--box-w) * .1061);left:calc(50% - var(--box-w) * .6739)}
  .prop-3{width:calc(var(--box-w) * .28);top:calc(var(--box-top) + var(--box-h) - var(--box-w) * .094);left:calc(50% + var(--box-w) * .406)}

  /* ink on plain paper, 20px above the box: never over the picture (the orphan dash and the bezel strike are gone) */
  .caption{top:auto;bottom:calc(100% - var(--box-top) + 20px);font-size:clamp(24px,6.6vw,30px);text-wrap:balance}
  /* the stage chip just above the box, the day plate just below it (the engine tweens only their opacity) */
  /* width:max-content: left:50% would otherwise cap their shrink-to-fit width at half the screen and wrap the plate */
  .phase{left:50%;right:auto;bottom:calc(100% - var(--box-top) + 16px);transform:translateX(-50%);width:max-content}
  .day{left:50%;right:auto;bottom:auto;top:min(calc(var(--box-top) + var(--box-h) + 18px), calc(var(--vis) - 96px));transform:translateX(-50%);width:max-content;text-align:center;padding:12px 18px 10px}
  .day-num{font-size:34px}
  .loader{left:50%;bottom:auto;top:min(calc(var(--box-top) + var(--box-h) + 18px), calc(var(--vis) - 92px));transform:translateX(-50%);width:max-content}

  /* reduced / static: the same box in both flat panels; panel two has caption 1 above it, caption 2 below */
  .story--flat.story--two>.caption-2{top:calc(var(--box-top) + var(--box-h) + 22px);bottom:auto}
  .story--flat .day,.story--flat .phase{display:none}
  .story--flat::before{display:none}
}
/* the stage space that is ALWAYS visible: the small viewport, so no browser toolbar ever covers the copy */
@supports (height:100svh){
  @media (max-width:779px) and (orientation:portrait){ .story{--vis:100svh} }
}
@media (max-width:359px) and (orientation:portrait){ .hero .kicker{letter-spacing:.12em} }

/* ── LANDSCAPE PHONES (PHONE_LAND): the story stays full-bleed (the cover fit already shows the whole width) ──
   vh first, svh second: a browser without svh keeps the vh line. */
@media (hover:none) and (pointer:coarse) and (orientation:landscape) and (max-height:500px){
  header.site{height:48px}
  header.site .nav a.keep,.nav-menu{min-height:44px}
  .hero .title{font-size:min(7vw,12vh);font-size:min(7vw,12svh)}
  .hero .kicker{margin-bottom:12px}
  .hero .sub{margin-top:14px}
  .hero .cue-word,.anno-3,.anno-4{display:none}
  .anno-1,.anno-2{top:58px}
  .prop-1{width:26vh;width:26svh;top:calc(48px + 13vh);top:calc(48px + 13svh);right:3vw}
  .prop-2{width:34vh;width:34svh;left:-4vw;bottom:4vh;bottom:4svh}
  /* left = prop-2's rotated right edge (-4vw + 35.7svh) + 3.8svh of air: the two never touch at any landscape aspect */
  .prop-3{width:34vh;width:34svh;left:calc(39.5vh - 4vw);left:calc(39.5svh - 4vw);bottom:-16vh;bottom:-16svh}
  .prop-4{width:28vh;width:28svh;right:1vw;bottom:2vh;bottom:2svh}
  .caption{top:calc(48px + 3vh);top:calc(48px + 3svh);font-size:min(4.6vw,8.5vh);font-size:min(4.6vw,8.5svh)}
  /* the counter and the chip move LEFT, over the plant and the books: the phone stays clear */
  .day{left:var(--gut);right:auto;text-align:left;padding:8px 12px 7px;bottom:14px;bottom:calc(14px + 100vh - 100svh)}
  .day-num{font-size:30px}
  .day .day-of{font-size:11px;letter-spacing:.16em}
  .anno{font-size:11px}
  .phase{bottom:85px;bottom:calc(85px + 100vh - 100svh)}
  /* the bottom band lifts above a landscape toolbar */
  .loader{bottom:14px;bottom:calc(14px + 100vh - 100svh)}
  .hero .cue{bottom:14px;bottom:calc(14px + 100vh - 100svh)}
  /* reduced / static: panel two is 390px tall, the two captions fill it; the chip and the plate would sit
     under caption 2 (as in the flat portrait stage, they are left out) */
  .story--flat .day,.story--flat .phase{display:none}
  /* the pair cards side by side, the disc on the vertical seam */
  .pair-card{grid-template-columns:1fr 1fr;grid-template-rows:none}
  .pair-card .half h2{font-size:clamp(24px,3vw,44px)}
  .pair-card .half:not(.dark)::before{top:50%;left:0}
}

/* ── LIFE below 780px: the window is a plain full-width frame (the engine's inline polygon is overridden;
   WINDOW_SHAPE itself is untouched) ── */
@media (max-width:779px){
  .life .window{clip-path:none!important;-webkit-clip-path:none!important;height:110vw}
}

/* ── the form on a touch screen (tablets too, where the sheet floats): a backdrop swipe must not move the
   page under the sheet, the × rides along, and "Merci" sits at the middle of the sheet ── */
@media (pointer:coarse){
  html.brief-open{overflow:hidden}
  .brief-close{position:sticky;top:12px;right:auto;align-self:flex-end;margin-bottom:-72px}
  .brief-done{min-height:100%;padding-top:calc(clamp(26px,5vw,58px) + min(96px, 20vh))}
}
