/* ==========================================================================
   motion.css — motion layer (DECISIONS 37–39). Colours, fonts and timing come
   from tokens.css only. Rule: every rule outside `.js-motion` is the FINAL,
   static state, so the page is complete without JS and with
   prefers-reduced-motion (motion.js then never adds html.js-motion).
   ========================================================================== */

/* ---------- 1. Home hero: photo + line art registered in one 3:2 stage ---------- */
/* .hero-frame is the size container (absolute, so a long title can still grow
   the hero). The stage reproduces object-fit: cover; object-position: 50% 40%
   for a 3:2 photo, so the SVG drawn in the photo's 1200x800 space stays on the
   house at any viewport. Overscan (scale 1.03) leaves room for the pointer offset. */
.hero-frame { position: absolute; inset: 0; overflow: hidden; container-type: size; }
.hero-stage { --sw: max(100vw, 150vh); --sw: max(100cqw, 150cqh); position: absolute; width: var(--sw); height: calc(var(--sw) / 1.5); left: calc((100cqw - var(--sw)) / 2); top: calc((100cqh - var(--sw) / 1.5) * .4); }
.hero-stage .hero-media { z-index: 0; transform: scale(1.03); will-change: transform; }
.hero-stage .hero-media::after { display: none; }
.hero-shade { position: absolute; inset: 0; z-index: 1; background: var(--hero-fade); pointer-events: none; }
.hero-lines { position: absolute; inset: 0; z-index: 2; width: 100%; height: 100%; max-width: none; transform: scale(1.03); opacity: .55; pointer-events: none; overflow: visible; will-change: transform; }
.hero-lines path { fill: none; stroke: var(--gold); stroke-width: 1; vector-effect: non-scaling-stroke; stroke-linecap: square; }
.hero--home .hero-text, .hero--home .hero-cue { z-index: 3; }
/* Pointer network (hero-network.js, DECISIONS 46): above photo and line art, below the text; home, desktop only. */
.hero-net { position: absolute; inset: 0; z-index: 2; width: 100%; height: 100%; pointer-events: none; opacity: 0; transition: opacity 1.2s var(--ease); }
.hero-net.is-on { opacity: 1; }
@media (prefers-reduced-motion: reduce), (hover: none), (pointer: coarse), (max-width: 767px) { .hero-net { display: none; } }
/* Phones: main outline only — the 15 strokes (.m) inside the centre crop. */
@media (max-width: 600px) { .hero-lines path:not(.m) { display: none; } }

/* Veil: the photo develops out of the paper (--bg, all heroes). Final state: invisible. */
.hero-veil { position: absolute; inset: 0; z-index: 1; background: var(--bg); opacity: 0; pointer-events: none; }
.hero--page .hero-media picture { transform-origin: 50% 40%; will-change: transform; }
.hero h1 .w { display: inline-block; }

/* ---------- 2. Scroll progress hairline ---------- */
.progress { position: fixed; inset: 0 0 auto 0; z-index: 60; height: 1px; background: var(--gold); transform-origin: 0 50%; transform: scaleX(0); pointer-events: none; display: none; }

/* ---------- 3. Hairlines beside numerals and labels (final state: drawn) ---------- */
.split-aside .num, .sec-head .num, .project-head .label { display: inline-flex; align-items: center; gap: 1.25rem; }
.sec-head .num { display: flex; }
.split-aside .num::after, .sec-head .num::after, .project-head .label::after { content: ""; flex: none; width: 3rem; height: 1px; background: var(--gold); transform-origin: 0 50%; }
.project-head .label { display: flex; }
.eyebrow::before { transform-origin: 0 50%; }

/* ---------- 4. Project images: hover grid + label (desktop, precise pointer) ---------- */
.project-grid li:hover img, .gallery li:hover img { transform: scale(1.03); }
@media (prefers-reduced-motion: reduce) {
  .feature:hover .feature-media img, .project-grid li:hover img, .gallery li:hover img { transform: none; }
}
.gallery-cap { z-index: 3; }
@media (hover: hover) and (pointer: fine) and (min-width: 601px) {
  a.feature-media { position: relative; isolation: isolate; }
  .project-grid li, .gallery li { isolation: isolate; }
  a.feature-media::after, .project-grid li::after, .gallery li::after {
    content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none; opacity: 0;
    background-image: linear-gradient(to right, var(--gold) 1px, transparent 1px), linear-gradient(to bottom, var(--gold) 1px, transparent 1px);
    background-size: 56px 56px; background-position: -1px -1px; transition: opacity .9s var(--ease);
  }
  [data-tag]::before {
    content: attr(data-tag); position: absolute; right: 1rem; bottom: .9rem; z-index: 2; pointer-events: none;
    padding: .45rem .6rem; background: var(--bg-tag); color: var(--tag-ink);
    font: 500 .62rem/1 var(--font-sans); letter-spacing: .22em; text-transform: uppercase;
    opacity: 0; transform: translateY(6px); transition: opacity .6s var(--ease), transform .6s var(--ease);
  }
  /* Renders: the "Architectural visualisation" caption keeps the bottom-right corner. */
  li:has(.gallery-cap)[data-tag]::before { top: .9rem; bottom: auto; }
  a.feature-media:hover::after, .project-grid li:hover::after, .gallery li:hover::after { opacity: .2; }
  [data-tag]:hover::before { opacity: 1; transform: none; }
}

/* ---------- 5. Initial states — only while JS runs and motion is allowed ---------- */
.js-motion .progress { display: block; }

/* hero intro (motion.js drives opacity/transform inline, then leaves the final values) */
.js-motion .hero-veil { opacity: .94; }
.js-motion .hero-lines { opacity: 1; }
.js-motion .hero-lines path { stroke-dasharray: 1 1; stroke-dashoffset: 1; }
.js-motion .hero [data-intro], .js-motion .hero [data-intro-lines]:not(.is-split) { opacity: 0; }
.js-motion .hero .w { opacity: 0; }
.js-motion .hero .eyebrow::before { transform: scaleX(0); transition: transform 1.3s var(--ease) .2s; }
.js-motion .hero .eyebrow.is-in::before { transform: none; }

/* content blocks: fade up once */
.js-motion [data-reveal]:not(.band):not(.cta),
.js-motion .band[data-reveal] > div:not(.band-media) > *,
.js-motion .cta[data-reveal] > .wrap > * {
  opacity: 0; transform: translateY(28px);
  transition: opacity 1.1s var(--ease), transform 1.1s var(--ease);
  transition-delay: calc(var(--d, 0s) + var(--k, 0s));
}
.js-motion [data-reveal].is-in:not(.band):not(.cta),
.js-motion .band.is-in > div:not(.band-media) > *,
.js-motion .cta.is-in > .wrap > * { opacity: 1; transform: none; }
.js-motion .band[data-reveal] > div > :nth-child(2), .js-motion .cta[data-reveal] > .wrap > :nth-child(2) { --k: .12s; }
.js-motion .band[data-reveal] > div > :nth-child(3), .js-motion .cta[data-reveal] > .wrap > :nth-child(3) { --k: .24s; }
.js-motion .band[data-reveal] > div > :nth-child(n+4), .js-motion .cta[data-reveal] > .wrap > :nth-child(n+4) { --k: .36s; }

/* hairlines draw left to right after their block arrives */
.js-motion [data-reveal] .num::after, .js-motion [data-reveal] .label::after, .js-motion [data-reveal] .eyebrow::before {
  transform: scaleX(0); transition: transform 1.3s var(--ease) calc(var(--d, 0s) + .35s);
}
.js-motion [data-reveal].is-in .num::after, .js-motion [data-reveal].is-in .label::after, .js-motion [data-reveal].is-in .eyebrow::before { transform: none; }

/* ---------- 6. Cursor dot and halo (cursor.js; DECISIONS 51, 53, 83, 85; review/gold-fix-2026-10-22.md) ---------- */
/* Created by JS only on (hover:hover) and (pointer:fine) with motion allowed, so there is no static state (reduced motion,
   touch-only and no-JS: neither element exists). Mouse only (Pointer Events): a touch or pen event hides both until the
   mouse moves again, so a touch laptop never shows a stray dot. Fades out on window blur / pointer leaving the window.
   Dot (DECISIONS 85, boss: "solid disc stays translucent"): 28px, --cursor-dot (alpha .45). Follow: rAF lerp 0.18 per
   60 Hz frame (frame-rate independent), about 97% of a jump in 0.3 s. Hover a, button, [role=button], label[for],
   .gallery-link, .feature-media, project tiles: grows x1.2 (34px; ring 36px); snaps to the target's centre only if it is at
   most 240 x 96px, so photos and cards just grow.
   Halo (DECISIONS 85, after TDK proto-v6): --halo-size 820px, --halo-fill (tokens.css), trails the pointer at 0.4 x the
   dot's lerp factor; it does not grow or snap. Boss 2026-10-22 (standard for all sites): "keep the size, put it under the
   text, do not wash the text out": the halo sits under the content (below), so it shows on the warm-white ground and is
   covered by photos, the band, the footer (--bg-2) and the header glass. It still darkens the ground behind letters, so
   --gold-text was deepened to #7A5D28 to stay >= 4.5 under halo + dot (review/gold-fix-2026-10-22.md, three methods compared).
   Dot over text: mix-blend-mode multiply (it can only darken, never lifts dark letters towards gold; plain alpha blending
   took gold small text from 5.03 to 2.42:1) and opacity .33 (alpha .15) away from photographs, snapped or not (DECISIONS 53:
   letters show through). .is-photo (pointer on an img, or on the bare .hero-text box, which spans the whole hero, at least
   40px clear of its eyebrow, title and lede boxes) gives it full strength (.45).
   Layer: dot 90, above the header (50) and mobile menu (60/70), below the skip link (100). A modal lightbox <dialog> sits
   in the top layer, so cursor.js moves both inside it while it is open.
   pointer-events: none keeps every control underneath clickable. Position and size: JS transform only. */
.cursor, .cursor-halo { position: fixed; left: 0; top: 0; z-index: 90; border-radius: 50%; pointer-events: none; opacity: 0; transition: opacity .3s var(--ease); will-change: transform; }
.cursor { width: 28px; height: 28px; margin: -14px 0 0 -14px; background: var(--cursor-dot); mix-blend-mode: multiply; }
/* Halo under the content: z-index -1 in the root stacking context paints above the canvas and below every in-flow box and all text. Only the html
   ground (--bg) may sit under it, so body drops its own identical --bg while the halo exists. Photos, the band, the footer
   (--bg-2) and the header glass cover it. Normal blending: nothing is drawn over letters. */
.cursor-halo { z-index: -1; width: var(--halo-size); height: var(--halo-size); margin: calc(var(--halo-size) / -2) 0 0 calc(var(--halo-size) / -2); background: var(--halo-fill); }
body:has(> .cursor-halo) { background: transparent; }
/* Preview variant ?cursor=ring (not public, DECISIONS 53): 28px outline, 36px on hover. Colour --gold-logo: the 1.5px line needs
   the deeper gold on the light ground. Stroke is an inset shadow (a 1.5px border snaps to 1px on 1x screens) and is thinned while scaled up. */
.cursor--ring { background: none; box-shadow: inset 0 0 0 1.5px var(--gold-logo); transition: opacity .3s var(--ease), box-shadow .25s var(--ease); }
.cursor--ring.is-hot { box-shadow: inset 0 0 0 calc(1.5px * 28 / 36) var(--gold-logo); }
.cursor.is-on { opacity: .33; }
.cursor.is-on.is-photo, .cursor--ring.is-on, .cursor-halo.is-on { opacity: 1; }
@media (prefers-reduced-motion: reduce), (hover: none), (pointer: coarse) { .cursor, .cursor-halo { display: none; } }
