/* =========================================================================
   RESURE LUX — Home
   Tokens first. Every colour and size below is referenced by role, never by
   appearance, so a rebrand is a change here and nowhere else.
   ========================================================================= */

:root {
  /* --- CI palette (raw). Never use these directly in a component. --- */
  --ci-void:   #201b1b;   /* warm near-black */
  --ci-plum:   #372648;   /* deep purple — the brand colour */
  --ci-mist:   #b9c4e2;   /* periwinkle */
  --ci-paper:  #ecf2f4;   /* off-white */
  --ci-ash:    #d9d9d9;   /* light grey */
  --ci-signal: #a855f7;   /* violet — the 10% */

  /* --- Ground ramp. CI plum walked down to black, so "90% purple + black"
         reads as one family instead of two unrelated darks. The warm
         --ci-void is kept for reference but is too brown to carry a page. --- */
  --plum-900: #08060c;
  --plum-800: #0e0a14;
  --plum-700: #16101e;
  --plum-600: #241831;
  --plum-500: #372648;
  --plum-400: #4a3363;

  /* --- Semantic roles. Use these. --- */
  --bg:             var(--plum-900);
  --surface:        var(--plum-800);
  --surface-raised: var(--plum-700);
  --on-bg:          var(--ci-paper);
  --on-bg-muted:    var(--ci-mist);
  --on-bg-faint:    rgba(185, 196, 226, 0.66);
  --accent:         var(--ci-signal);
  /* The bright violet reads at 5.10:1 on the page black, which is what marks,
     rules and focus rings need. Under white text it is only 3.50:1, so filled
     buttons drop to these deeper steps of the same hue. */
  --accent-fill:    #7c3aed;   /* 5.04:1 with --ci-paper */
  --accent-fill-hi: #8340e0;   /* 4.92:1 with --ci-paper */
  --outline:        rgba(185, 196, 226, 0.15);
  --outline-strong: rgba(185, 196, 226, 0.32);

  /* --- Modular scale. Base 16px, ratio 1.333 (perfect fourth), extended at
         the top so display type can carry a whole screen. --- */
  --ms-caption: 0.75rem;
  --ms-small:   0.875rem;
  --ms-body:    1rem;
  --ms-h3:      1.3125rem;
  --ms-h2:      1.75rem;
  --ms-h1:      2.375rem;
  --ms-display: 3.125rem;
  --ms-mega:    5.5rem;

  /* --- Type. Cinzel is an inscriptional face — it carries the monumental
         register and is set in caps with air between the letters. Inter does
         the reading. (Cinzel's own companion, Josefin Sans, has too small an
         x-height to be trusted at 12–14px UI sizes.) --- */
  --font-display: 'Cinzel', 'Trajan Pro', Georgia, 'Times New Roman', serif;
  --font-body:    'Inter', -apple-system, 'Segoe UI', system-ui, sans-serif;
  --lh-body: 1.7;
  --lh-head: 1.08;

  /* --- Space. 8px grid. --- */
  --sp-1: 0.5rem;  --sp-2: 1rem;   --sp-3: 1.5rem;
  --sp-4: 2rem;    --sp-6: 3rem;   --sp-8: 4rem;   --sp-12: 6rem;

  /* --- Light and shadow. On a near-black ground a drop shadow is almost
         invisible — the eye reads depth from a *lit top edge* first and the
         ambient shadow second. Every elevation below is therefore a hairline
         highlight plus a deep, wide shadow, never a shadow alone. --- */
  --lift-1: inset 0 1px 0 rgba(185, 196, 226, 0.07),
            0 10px 30px -14px rgba(0, 0, 0, 0.95);
  --lift-2: inset 0 1px 0 rgba(185, 196, 226, 0.12),
            0 26px 60px -26px rgba(0, 0, 0, 1);
  --lift-bar: 0 18px 40px -28px rgba(0, 0, 0, 1);
  /* the accent only ever glows, never fills, outside of buttons */
  --glow-accent: 0 0 30px -6px rgba(168, 85, 247, 0.55);

  /* --- Motion. Cinematic: slow enters, nothing that snaps. --- */
  --dur-sm:   160ms;
  --dur-md:   320ms;
  --dur-lg:   700ms;
  --dur-xl:   1100ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in:  cubic-bezier(0.7, 0, 0.84, 0);

  /* --- Elevation scale. --- */
  --z-base: 10; --z-sticky: 20; --z-overlay: 30; --z-top: 50;

    --foot-h: 4.5rem;

  --shell:  1600px;
  --gutter: clamp(1.25rem, 4vw, 4rem);
  --radius: 0;   /* monumental reads as sharp, never rounded */
}

/* Grain. Inline SVG turbulence — no network request, no image file.
   Kept faint: at high opacity it turns a black page muddy grey. */
:root {
  --noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");

  /* Semé-de-lis: the fleur-de-lis strewn in half-drop, which is the French
     royal arms' own arrangement. Drawn once and mirrored about its centre so
     the two lateral petals are true reflections. Inline SVG — no image file. */
  --mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 500 840'%3E%3Cpath d='M90 790 L90 400 A 320 320 0 0 1 410 80' fill='none' stroke='%23000' stroke-width='100' stroke-linecap='round'/%3E%3C/svg%3E");

  --seme: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='166' viewBox='0 0 96 166'%3E%3Cg fill='%23b9a0e8'%3E%3Cg transform='translate(10,6) scale(0.48)'%3E%3Cpath d='M50 6 C 56 17, 61 30, 59 42 C 58 50, 54 55, 52 60 L 48 60 C 46 55, 42 50, 41 42 C 39 30, 44 17, 50 6 Z'/%3E%3Cpath d='M47 61 C 37 62, 24 56, 18 44 C 13 34, 17 24, 24 28 C 31 32, 29 43, 35 51 C 39 57, 44 59, 49 63 Z'/%3E%3Cg transform='translate(100,0) scale(-1,1)'%3E%3Cpath d='M47 61 C 37 62, 24 56, 18 44 C 13 34, 17 24, 24 28 C 31 32, 29 43, 35 51 C 39 57, 44 59, 49 63 Z'/%3E%3C/g%3E%3Crect x='32' y='60' width='36' height='7'/%3E%3Cpath d='M44 67 L 56 67 L 59 80 C 54 83, 46 83, 41 80 Z'/%3E%3C/g%3E%3Cg transform='translate(58,89) scale(0.48)'%3E%3Cpath d='M50 6 C 56 17, 61 30, 59 42 C 58 50, 54 55, 52 60 L 48 60 C 46 55, 42 50, 41 42 C 39 30, 44 17, 50 6 Z'/%3E%3Cpath d='M47 61 C 37 62, 24 56, 18 44 C 13 34, 17 24, 24 28 C 31 32, 29 43, 35 51 C 39 57, 44 59, 49 63 Z'/%3E%3Cg transform='translate(100,0) scale(-1,1)'%3E%3Cpath d='M47 61 C 37 62, 24 56, 18 44 C 13 34, 17 24, 24 28 C 31 32, 29 43, 35 51 C 39 57, 44 59, 49 63 Z'/%3E%3C/g%3E%3Crect x='32' y='60' width='36' height='7'/%3E%3Cpath d='M44 67 L 56 67 L 59 80 C 54 83, 46 83, 41 80 Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}

/* ============================== reset ============================== */

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body, h1, h2, h3, p, figure, ul, li { margin: 0; padding: 0; }
ul { list-style: none; }
img, svg, iframe { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
a { color: inherit; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------------------------- section snap ----------------------------
   Scoped to html.has-snap, which only the home page carries. A residence page
   is a reading page: snapping a long text section fights the reader.
   Proximity, never mandatory: mandatory snapping fights the reader on any
   section taller than the viewport and traps keyboard scrolling. Snapping is
   motion, so it is only offered to visitors who have not asked for less. */
@media (prefers-reduced-motion: no-preference) {
  html.has-snap { scroll-snap-type: y proximity; }
  .snap { scroll-snap-align: start; scroll-snap-stop: normal; }
}
/* Mandatory only where every section is genuinely one screen tall. Snapping a
   section taller than the window strands whatever sits in its middle, so the
   firm setting is scoped to the sizes that earn it; everything else keeps the
   forgiving proximity above. */
@media (prefers-reduced-motion: no-preference) and (min-width: 901px) and (min-height: 600px) {
  html.has-snap { scroll-snap-type: y mandatory; }
  .snap { scroll-snap-stop: always; }
}

/* ============================== base ============================== */

body {
  min-height: 100svh;
  margin: 0;
  background: var(--bg);
  color: var(--on-bg);
  font-family: var(--font-body);
  font-size: var(--ms-body);
  font-weight: 300;
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

h1, h2, h3 { font-family: var(--font-display); font-weight: 400; line-height: var(--lh-head); }

::selection { background: var(--accent); color: var(--ci-paper); }

:where(a, button, input, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.vis-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

.skip {
  position: absolute; left: var(--sp-2); top: -100vh; z-index: var(--z-top);
  padding: 0.8rem var(--sp-3);
  background: var(--accent-fill); color: var(--ci-paper);
  font-size: var(--ms-small); text-decoration: none;
}
.skip:focus { top: var(--sp-2); }

/* ---- shared: the small caps label that sets the monumental register ---- */
.label {
  font-family: var(--font-display);
  font-size: var(--ms-caption);
  font-weight: 500;
  letter-spacing: 0.44em;
  text-transform: uppercase;
  color: var(--on-bg-muted);
}

/* -------- the ground: black, a plum bloom, and a whisper of grain -------- */

.bg-decor { position: fixed; inset: 0; z-index: 0; pointer-events: none; }

/* The ground carries the semé and, under it, a treillage lattice — the diamond
   trellis of French panelling. Both are held at a few percent: this is a
   texture you notice on the second look, not a wallpaper. */
.bg-decor__lys {
  position: absolute; inset: 0;
  background-image:
    var(--seme),
    repeating-linear-gradient(60deg,
      rgba(185, 160, 232, 0.55) 0 1px, transparent 1px 48px),
    repeating-linear-gradient(-60deg,
      rgba(185, 160, 232, 0.55) 0 1px, transparent 1px 48px);
  background-size: 96px 166px, auto, auto;
  opacity: 0.045;
}
.bg-decor::before {           /* plum bloom behind the search field */
  content: ''; position: absolute;
  left: 50%; top: -30vh; transform: translateX(-50%);
  width: min(1400px, 140vw); height: min(1100px, 110vh);
  background:
    radial-gradient(50% 50% at 50% 50%,
      rgba(74, 51, 99, 0.58) 0%,
      rgba(55, 38, 72, 0.28) 42%,
      rgba(8, 6, 12, 0) 72%);
}
.bg-decor::after {            /* film grain */
  content: ''; position: absolute; inset: 0;
  background-image: var(--noise);
  background-size: 200px 200px;
  opacity: 0.05;
  mix-blend-mode: overlay;
}

.page { position: relative; z-index: var(--z-base); }
.shell {
  width: 100%; max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* ============================ masthead ============================ */

/* Fixed, because with snap sections the nav would otherwise scroll away with
   the first screen and never come back. The scrim keeps it legible over the
   film frame it floats across. */
.masthead {
  position: fixed; inset: 0 0 auto 0;
  z-index: var(--z-sticky);
  /* one x-height of clear space above and below the logotype */
  padding-block: clamp(0.95rem, 1.9vw, 1.55rem);
  background: linear-gradient(to bottom,
    rgba(8, 6, 12, 0.96) 0%, rgba(8, 6, 12, 0.86) 66%, rgba(8, 6, 12, 0) 100%);
  pointer-events: none;   /* the band is decoration; only its contents click */
}
.masthead__shell > * { pointer-events: auto; }
.masthead__shell {
  display: grid;
  grid-template-columns: 1fr auto 1fr;   /* the search stays optically centred
                                            whatever the mark and nav weigh */
  align-items: center;
  gap: clamp(1rem, 2.4vw, 2.5rem);
}
.masthead__shell > .nav { justify-self: end; }
:root { --masthead-h: 5.75rem; }   /* measured height of the fixed bar */
@media (max-width: 900px) { :root { --masthead-h: 8.25rem; } }

/* ============================ the logotype ============================
   The logo is a monoline lowercase mark; the page is Cinzel caps. They are two
   voices, so the logo gets its own air and is never set flush against display
   type at a matching size. It carries CI mist — never the violet accent, which
   is reserved for UI state. */
.wordmark {
  display: inline-flex; align-items: center;
  min-height: 44px;
  color: var(--ci-mist);
  text-decoration: none;
  transition: color var(--dur-sm) var(--ease-out);
}
.wordmark__svg {
  width: clamp(132px, 13vw, 190px);
  height: auto;
  overflow: visible;
}
/* The same link carries both cuts of the logo. The logotype is 132px at its
   smallest — half a phone's content width — so it can never share a line with
   the search field. The mark alone is under 20px and can. Only one is ever
   displayed; both are aria-hidden and the link's aria-label is the name. */
.wordmark__mark { display: none; width: auto; height: 32px; overflow: visible; }
.wordmark:hover { color: var(--ci-paper); }

/* The mark alone, for spaces too small or too square for the logotype. */
.foot__mark { display: inline-flex; align-items: center; gap: 0.85rem; }
.foot__mark svg { width: auto; height: 1.35em; color: var(--ci-mist); flex: none; }

.nav ul { display: flex; gap: clamp(1.25rem, 2.6vw, 2.5rem); }
.nav a {
  position: relative;
  display: inline-flex; align-items: center;
  min-height: 44px;
  font-size: var(--ms-caption);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--on-bg-faint);
  text-decoration: none;
  transition: color var(--dur-sm) var(--ease-out);
}
.nav a::after {
  content: ''; position: absolute; left: 0; right: 100%; bottom: 10px;
  height: 1px; background: var(--accent);
  transition: right var(--dur-md) var(--ease-out);
}
.nav a:hover, .nav a[aria-current='page'] { color: var(--on-bg); }
.nav a:hover::after, .nav a[aria-current='page']::after { right: 0; }



/* The brief asks for a field a quarter of the page wide. A literal 25%
   truncates the placeholder on a laptop, so 25% is the target with a floor
   that keeps the placeholder whole. */
/* Still the brief's quarter of the page, now living in the bar.
   The boxed panel and its solid slab of a button read like a generic form
   control against Cinzel and hairlines. Rewritten in the page's own language:
   a rule under the field rather than a box around it, and a ghost submit that
   only takes the accent on hover. */
.search {
  --search-w: 25vw;
  display: flex; align-items: center; gap: var(--sp-2);
  width: clamp(15rem, var(--search-w), 26rem);
  padding: 0 0 0 0.25rem;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--outline-strong);
  transition: border-color var(--dur-md) var(--ease-out);
}
.search:hover { border-bottom-color: rgba(185, 196, 226, 0.5); }
.search:focus-within { border-bottom-color: var(--accent); }
.search__icon {
  flex: none; color: var(--on-bg-faint);
  transition: color var(--dur-md) var(--ease-out);
}
.search:focus-within .search__icon { color: var(--on-bg-muted); }
.search__field {
  flex: 1 1 auto; min-width: 0;
  min-height: 44px;
  background: none; border: 0;
  font-size: var(--ms-small);
  letter-spacing: 0.06em;
  color: var(--on-bg);
}
/* Without the old glass panel behind it, the placeholder sits straight on the
   film still; --on-bg-faint measured 4.42:1 there. This step clears AA and is
   still plainly dimmer than entered text, which is --on-bg. */
.search__field::placeholder { color: rgba(185, 196, 226, 0.82); }
.search__field:focus { outline: none; }
.search__field::-webkit-search-cancel-button { filter: invert(1) opacity(0.5); }

.search__submit {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px; padding-inline: 1rem;   /* 44 is the floor, bar or not */
  border-left: 1px solid var(--outline);    /* separates it from the field */
  background: none;
  color: var(--on-bg-muted);
  font-family: var(--font-display);
  font-size: var(--ms-caption);
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  transition: color var(--dur-sm) var(--ease-out),
              background var(--dur-sm) var(--ease-out);
}
.search__submit-icon { display: none; }
.search__submit:hover { color: var(--ci-paper); }
/* Enter already submits; the button is the visible affordance, so it earns the
   accent only when pointed at. */
.search__submit:hover, .search__submit:focus-visible {
  background: var(--accent-fill);
  color: var(--ci-paper);
}

/* ------------------------- the film: full bleed -------------------------
   The still covers the whole window; object-fit crops it rather than letting
   the 16:9 source letterbox. The player itself is a different matter — see
   .stage__frame iframe below. */
/* First screen, edge to edge: no frame, no gutter, nothing around it. */
.film { height: 100svh; }
.film .stage, .film .stage__frame { width: 100%; height: 100%; }
.film .stage__frame { aspect-ratio: auto; box-shadow: none; background: var(--bg); }
.stage { width: 100%; margin-inline: auto; }

/* The slug used to sit under the frame; on a full-height screen that extra row
   is what would push an 80% film off the edge, so it moved inside. */
.stage__slug {
  position: absolute; z-index: 2;
  top: clamp(0.9rem, 2.2vw, 1.75rem); left: clamp(1.25rem, 3.2vw, 3rem);
  font-family: var(--font-display);
  font-size: var(--ms-caption);
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ci-paper);
  text-shadow: 0 2px 16px rgba(0, 0, 0, 0.9);
  pointer-events: none;
}

.stage__frame {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--surface);
  box-shadow: 0 60px 140px -60px rgba(0, 0, 0, 1),
              inset 0 1px 0 rgba(185, 196, 226, 0.08),
              0 0 0 1px var(--outline);
}

/* Facade: a still plus a play button. YouTube's iframe — and its cookies —
   is only fetched when someone actually asks for the film. */
.poster {
  position: absolute; inset: 0;
  display: block; width: 100%; height: 100%;
  padding: 0; text-align: left; color: var(--on-bg);
  cursor: pointer;
}
.poster__img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform var(--dur-xl) var(--ease-out);
}
/* The cover keeps the film's own colour — no tint over it. The veil is
   neutral black at low opacity purely so the caption stays readable. */
.poster__veil {
  position: absolute; inset: 0;
  background:
    radial-gradient(72% 60% at 88% 88%,
      rgba(0, 0, 0, 0.82) 0%, rgba(0, 0, 0, 0.4) 46%, rgba(0, 0, 0, 0) 100%),
    linear-gradient(to top,
      rgba(0, 0, 0, 0.86) 0%,
      rgba(0, 0, 0, 0.44) 30%,
      rgba(0, 0, 0, 0.04) 58%,
      /* the frame's top band is a white wall in this film; the slug needs it
         carried down hard so the slug clears 4.5:1 on a light frame */
      rgba(0, 0, 0, 0.68) 100%);
}
.poster:hover .poster__img,
.poster:focus-visible .poster__img { transform: scale(1.04); }

.poster__body {
  position: absolute; inset: auto 0 0 0;
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--sp-4);
  width: 100%; max-width: var(--shell);
  margin-inline: auto;
  padding: clamp(1.5rem, 4vw, 4rem) var(--gutter);
}
.poster__title {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 2.1vw, 2rem);
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.28;
  max-width: 20ch;
  text-shadow: 0 2px 24px rgba(0, 0, 0, 0.75);
}
.poster__meta {
  display: block;
  margin-top: 0.6rem;
  font-size: var(--ms-caption);
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--ci-paper);
  opacity: 0.72;
}
.poster__play {
  flex: none;
  display: inline-flex; align-items: center; gap: 0.8rem;
  min-height: 52px; padding: 0.6rem 1.75rem;
  border: 1px solid rgba(236, 242, 244, 0.5);
  background: rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  font-family: var(--font-display);
  font-size: var(--ms-caption);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  transition: background var(--dur-md) var(--ease-out),
              border-color var(--dur-md) var(--ease-out);
}
.poster:hover .poster__play,
.poster:focus-visible .poster__play {
  background: var(--accent-fill);
  border-color: var(--accent-fill);
  color: var(--ci-paper);
  box-shadow: var(--glow-accent);
}

/* The window is wider than 16:9, so the player is given the largest 16:9 box
   that fits and is centred — the surround is the page's own black. */
.stage__frame iframe {
  position: absolute; inset: 0; margin: auto;
  width: min(100%, calc(100svh * 1.7778));
  height: min(100%, calc(100vw * 0.5625));
  border: 0;
}

.stage__caption {
  display: flex; justify-content: space-between; gap: var(--sp-3);
  flex-wrap: wrap;
  margin-top: var(--sp-2);
  font-size: var(--ms-caption);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--on-bg-faint);
}
.stage__caption span:first-child { font-family: var(--font-display); letter-spacing: 0.3em; }

/* A quiet instruction that there is more than one screen — without it a
   mandatory-snap cover reads as the whole site. */

.scroll-cue {
  position: absolute; z-index: 2;
  left: 50%; bottom: clamp(0.75rem, 2vw, 1.5rem);
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 0.6rem;
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.9);
  pointer-events: none;
  font-family: var(--font-display);
  font-size: var(--ms-caption);
  letter-spacing: 0.4em;
  text-transform: uppercase;
  color: var(--on-bg-faint);
}
.scroll-cue::after {
  content: ''; width: 1px; height: clamp(1.75rem, 4vw, 3rem);
  background: linear-gradient(var(--outline-strong), transparent);
}
@media (prefers-reduced-motion: no-preference) {
  .scroll-cue::after { animation: cue 2.6s var(--ease-out) infinite; transform-origin: top; }
  @keyframes cue { 0%,100% { transform: scaleY(0.45); opacity: 0.4; } 50% { transform: scaleY(1); opacity: 1; } }
}

/* ================= 4. the residences — text over the image ================= */

.projects {
  height: calc(100svh - var(--foot-h));
  padding-block: clamp(1rem, 2.2vw, 2rem);
}
.projects__shell {
  height: 100%;
  display: flex; flex-direction: column; justify-content: center;
  padding-top: var(--masthead-h);
}

.section-head {
  flex: none;
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--sp-6); flex-wrap: wrap;
  padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--outline);
  margin-bottom: clamp(1rem, 2.2vw, 1.75rem);
}
.section-head__title { display: flex; flex-direction: column; gap: var(--sp-2); }
.section-head h2 {
  font-size: clamp(1.75rem, 3.4vw, 3.5rem);
  font-weight: 400;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.section-head p {
  max-width: 40ch;
  font-size: var(--ms-small);
  color: var(--on-bg-faint);
}

.pf { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; }
.pf__viewport { overflow: hidden; flex: 1; min-height: 16rem; }
.pf__track { height: 100%; }
.pf__slide { height: 100%; }
.pf__track {
  display: flex;
  transition: transform var(--dur-lg) var(--ease-out);
  will-change: transform;
}
.pf__slide { flex: 0 0 100%; min-width: 0; }

/* Fills whatever height the flex column leaves. object-fit does the cropping,
   so the frame can be any proportion the window ends up giving it. */
.pf__link {
  position: relative; display: block;
  height: 100%;
  overflow: hidden;
  text-decoration: none;
  background: var(--surface);
  box-shadow: 0 0 0 1px var(--outline);
}
.pf__img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  transform: scale(1.04);
  transition: transform 1200ms var(--ease-out);
}
.pf__link:hover .pf__img,
.pf__link:focus-visible .pf__img { transform: scale(1.1); }

/* These photographs keep their own colour, so the scrim doing the work of
   legibility has to be neutral black and has to be strong — a daylight frame
   under white type needs far more cover than a dusk one. */
.pf__scrim {
  position: absolute; inset: 0;
  background:
    linear-gradient(to top,
      rgba(0, 0, 0, 0.92) 0%,
      rgba(0, 0, 0, 0.68) 30%,
      rgba(0, 0, 0, 0.22) 58%,
      rgba(0, 0, 0, 0.05) 80%),
    linear-gradient(to right,
      rgba(0, 0, 0, 0.86) 0%,
      rgba(0, 0, 0, 0.5) 32%,
      rgba(0, 0, 0, 0.05) 62%,
      rgba(0, 0, 0, 0) 100%);
}

/* ------------------------------ watermark ------------------------------
   The mark, never the logotype: at watermark weight the logotype's hairlines
   disappear, and it would compete with the caption. Top-right is the only free
   corner — the caption owns bottom-left and the carousel nav bottom-right.
   Painted through a mask so one asset serves any colour or opacity. */
.pf__wm {
  position: absolute; z-index: 2;
  top: clamp(1rem, 2.6vw, 2.25rem);
  right: clamp(1rem, 2.6vw, 2.25rem);
  /* ~7% of the frame's height — a corner watermark is judged by height, and
     sizing off the width made it 11.5% on a wide frame */
  height: clamp(22px, 4.5vh, 40px);
  aspect-ratio: 500 / 840;
  background: var(--ci-paper);
  opacity: 0.58;
  -webkit-mask: var(--mark) center / contain no-repeat;
  mask: var(--mark) center / contain no-repeat;
  /* a photograph can be white where the mark lands, so it carries its own shadow */
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.75));
  pointer-events: none;
}

.pf__caption {
  position: absolute; inset: auto 0 0 0;
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 0.6rem;
  padding: clamp(1.25rem, 3.6vw, 3.5rem);
  color: var(--ci-paper);
}
.pf__index {
  font-family: var(--font-display);
  font-size: var(--ms-caption);
  font-weight: 500;
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--ci-paper);
  opacity: 0.82;
}
.pf__name {
  font-size: clamp(1.75rem, 4vw, 3.75rem);
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-shadow: 0 4px 40px rgba(0, 0, 0, 0.85);
}
.pf__tagline {
  max-width: 34ch;
  font-size: var(--ms-small);
  letter-spacing: 0.02em;
  color: var(--ci-paper);
  opacity: 0.86;
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.85);
}
.pf__facts {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 0.35rem;
  font-size: var(--ms-small);
  color: var(--ci-paper);
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.85);
}
.pf__sep { width: 3px; height: 3px; border-radius: 50%; background: currentColor; opacity: 0.5; }
.pf__price { font-weight: 400; }

.pf__tag {
  display: inline-flex; align-items: center; gap: 0.55rem;
  margin-top: 0.15rem;
  padding: 0.3rem 0.95rem;
  border: 1px solid rgba(236, 242, 244, 0.42);
  font-size: var(--ms-caption);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  background: rgba(0, 0, 0, 0.42);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
.pf__dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); flex: none; }
/* State is carried by the wording too, never by the dot colour alone. */
.pf__tag[data-state='soon'] .pf__dot { background: var(--ci-mist); }

/* ------- controls: a thumbnail per residence, plus prev / next ------- */

.pf__bar {
  flex: none;
  display: flex; align-items: flex-end; justify-content: flex-start;
  margin-top: var(--sp-3);
}

.pf__thumbs { display: flex; gap: clamp(0.5rem, 1.2vw, 1rem); flex-wrap: wrap; }
.pf__thumb {
  position: relative;
  display: flex; flex-direction: column; gap: 0.55rem;
  width: clamp(112px, 12vw, 172px);
  padding: 0;
  text-align: left;
  color: var(--on-bg-faint);
  transition: color var(--dur-md) var(--ease-out);
}
.pf__thumb-img {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  box-shadow: 0 0 0 1px var(--outline);
}
.pf__thumb-img img {
  width: 100%; height: 100%; object-fit: cover;
  /* Unselected thumbnails step back so the current one is unmistakable. */
  filter: grayscale(0.6) brightness(0.5);
  transition: filter var(--dur-md) var(--ease-out),
              transform var(--dur-lg) var(--ease-out);
}
.pf__thumb-img::after {          /* the selected marker, drawn on the image */
  content: ''; position: absolute; inset: auto 0 0 0;
  height: 2px; background: var(--accent);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--dur-md) var(--ease-out);
}
.pf__thumb-label {
  display: flex; align-items: baseline; gap: 0.5rem;
  position: relative;
  font-family: var(--font-display);
  font-size: var(--ms-caption);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.pf__thumb-num { opacity: 0.7; min-width: 2ch; }

.pf__thumb:hover { color: var(--on-bg-muted); }
.pf__thumb:hover .pf__thumb-img img { filter: grayscale(0.2) brightness(0.78); }
.pf__thumb[aria-selected='true'] { color: var(--on-bg); }
.pf__thumb[aria-selected='true'] .pf__thumb-img img { filter: none; }
.pf__thumb[aria-selected='true'] .pf__thumb-img::after {
  transform: scaleX(1);
  box-shadow: var(--glow-accent);
}
.pf__thumb[aria-selected='true'] .pf__thumb-img { box-shadow: var(--lift-2); }


/* ============================== footer ============================== */

.foot {
  min-height: var(--foot-h);
  display: flex; align-items: center;
  padding-block: var(--sp-3);
  border-top: 1px solid var(--outline);
  font-size: var(--ms-caption);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--on-bg-faint);
}
/* These sit on one inline row, so the tap area is grown with a pseudo-element
   instead of padding — 44px of target without 44px of layout. */
.foot__shell {
  width: 100%;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); flex-wrap: wrap;
}
.foot a {
  position: relative; display: inline-block;
  text-decoration: none;
  transition: color var(--dur-sm) var(--ease-out);
}
.foot a::after { content: ''; position: absolute; inset: -15px -10px; }
.foot a:hover { color: var(--on-bg); }

/* ========================== entrance motion ========================== */

/* Reveal. opacity + transform only, so nothing reflows and nothing shifts the
   page as it settles. --i staggers a group; the delay is capped so a long list
   never leaves the last item waiting. */
.js .rise { opacity: 0; transform: translateY(26px); }
.js .rise.is-in {
  opacity: 1; transform: none;
  transition: opacity var(--dur-xl) var(--ease-out),
              transform var(--dur-xl) var(--ease-out);
  transition-delay: calc(min(var(--i, 0) * 90ms, 360ms));
}
@media (prefers-reduced-motion: reduce) {
  .js .rise { opacity: 1; transform: none; }
}

/* ------------------------- shared elevation ------------------------- */

/* Hover lift. transform moves the card without touching layout, so nothing
   around it reflows. box-shadow is not GPU-composited, but this is a hover on
   at most a handful of visible cards — not a running animation. */
.lift {
  box-shadow: var(--lift-1);
  transition: box-shadow var(--dur-md) var(--ease-out),
              border-color var(--dur-md) var(--ease-out),
              transform var(--dur-md) var(--ease-out);
}
.lift:hover,
.lift:focus-visible,
.lift:focus-within {
  box-shadow: var(--lift-2);
  border-color: var(--outline-strong);
  transform: translateY(-2px);
}
@media (prefers-reduced-motion: reduce) {
  .lift:hover, .lift:focus-visible, .lift:focus-within { transform: none; }
}

/* The bar floats over content, so once the page has moved it earns an edge.
   Toggled from JS rather than backdrop tricks, so it is one class either way. */
.masthead::after {
  content: ''; position: absolute; inset: 0;
  box-shadow: var(--lift-bar);
  opacity: 0;
  transition: opacity var(--dur-md) var(--ease-out);
  pointer-events: none;
}
.masthead.is-scrolled::after { opacity: 1; }
.masthead.is-scrolled { border-bottom: 1px solid var(--outline); }

/* ============================ responsive ============================ */

/* Wide screens: the nav rides on the frame, bottom-right, opposite the
   caption. The frame's own scrim is already darkest there, so the thumbnails
   sit on cover without needing one of their own. */
@media (min-width: 901px) {
  .pf { position: relative; }
  /* One row hard against the bottom edge, where the frame's scrim is deepest —
     stacked, the thumbnails floated into the middle of the picture where there
     is no cover at all. */
  .pf__bar {
    position: absolute; z-index: 2;
    right: clamp(1.25rem, 3vw, 3rem);
    bottom: clamp(1rem, 2.4vw, 2.25rem);
    left: auto;
    margin-top: 0;
    width: auto;
    flex-direction: row; align-items: flex-end;
    flex-wrap: nowrap;
  }
  .pf__thumbs { justify-content: flex-end; flex-wrap: nowrap; }
  .pf__thumb { width: clamp(84px, 7vw, 124px); }
  .pf__thumb-img { box-shadow: 0 0 0 1px rgba(236, 242, 244, 0.28); }
  .pf__thumb-label { color: var(--ci-paper); text-shadow: 0 2px 12px rgba(0, 0, 0, 0.9); }
  /* "SUKHUMVIT" is wider than the thumbnail it labels, so the names collide.
     Clipped rather than display:none — the button's accessible name has to
     stay "I Sukhumvit", not a bare "I". */
  .pf__thumb-label span:last-child {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip-path: inset(50%); overflow: hidden; white-space: nowrap;
  }
  .pf__thumb-num { min-width: 0; opacity: 1; }
  /* the caption must not run under the thumbnails */
  .pf__caption { max-width: 44%; }
}

/* Between 900 and 1100 there is not room for four named thumbnails beside the
   caption, so the names drop and the numerals carry the nav. */
@media (min-width: 901px) and (max-width: 1100px) {
  .pf__thumb { width: 76px; }
  .pf__caption { max-width: 44%; }
  .pf__name { font-size: clamp(1.6rem, 3.6vw, 2.5rem); }
}

@media (max-width: 900px) {
  /* Swapping the logotype for the mark buys back ~112px, which is enough for
     the logo and the field to share the top line. The nav still cannot join
     them — at 360px it needs more width than the field would leave — so the
     bar is two rows rather than three. */
  .masthead__shell {
    /* minmax(0,1fr), not 1fr: a bare 1fr takes an automatic min-content floor
       from the field, and the submit button pushes that past the track, so the
       word SEARCH gets clipped by the right gutter. */
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas: 'mark search' 'nav nav';
    column-gap: 0.75rem;   /* the mark's target to the field's: 12px, min is 8 */
    row-gap: 0.5rem;
    justify-items: stretch;
  }
  /* A 44px box with the mark flush to its leading edge: a full-size target
     that still starts on the gutter, so the logo is not visually indented. */
  .wordmark {
    grid-area: mark;
    min-width: 44px; min-height: 44px;
    justify-content: flex-start;
  }
  .wordmark__svg { display: none; }
  .wordmark__mark { display: block; }
  .nav a::before { content: ''; position: absolute; inset: -4px -6px; }
  .masthead__shell > .search { grid-area: search; }
  .masthead__shell > .nav { grid-area: nav; justify-self: start; }
  .search { --search-w: 100%; width: 100%; min-width: 0; }
  .nav ul { gap: clamp(0.9rem, 4vw, 1.5rem); flex-wrap: nowrap; }
  .nav a { min-height: 36px; font-size: 0.6875rem; letter-spacing: 0.12em; }

  .poster__body { flex-direction: column; align-items: flex-start; gap: var(--sp-2); }
  /* stacked, the caption and play button already own the bottom of the frame */
  .scroll-cue { display: none; }
  .section-head { align-items: flex-start; }
  .pf__viewport { flex: none; min-height: 0; }
  .pf__link { height: auto; aspect-ratio: 16 / 9; }
  .poster__body { flex-direction: column; align-items: flex-start; gap: var(--sp-2); }
  .section-head { align-items: flex-start; }
  .pf__viewport { flex: none; min-height: 0; }
  .pf__link { height: auto; aspect-ratio: 16 / 9; }
}

@media (max-width: 720px) {
  .projects { height: auto; min-height: 0; }
  .foot { min-height: 0; }
  .nav ul { gap: var(--sp-2); flex-wrap: wrap; }
  .nav a { min-height: 36px; }
  .stage { width: 100%; }
  .pf__link { height: auto; aspect-ratio: 3 / 4; }
  .pf__caption, .poster__body { padding: var(--sp-2); }

  .pf__bar { display: block; }
  .pf__thumbs { display: grid; grid-template-columns: repeat(4, 1fr); }
  .pf__thumb { width: auto; }
  .pf__thumb-label { font-size: 0.6875rem; letter-spacing: 0.1em; }
  .foot__shell { flex-direction: column; align-items: flex-start; }
}

@media (max-width: 420px) {
  /* Stacking the button under the field made sense when the search was a hero
     element; inside a bar it doubles the bar's height. Keep one row and take
     the space out of the padding instead. The left inset was clearance for the
     magnifier, which now sits on the button, so the field starts flush and the
     placeholder survives down to 320px. */
  .search { flex-wrap: nowrap; padding: 0.3rem 0.3rem 0.3rem 0; gap: 0.5rem; }
  .search__field { flex: 1 1 auto; min-width: 0; padding-inline: 0; }
  /* Below this width the word SEARCH (98px) and the decorative magnifier (33px
     with its gap) cost more than the placeholder can spare, so the button takes
     the magnifier and the decorative one stands down — two magnifiers in one
     field would say nothing twice anyway. The button keeps its 44px target and
     its accessible name. */
  .search__icon { display: none; }
  .search__submit-text { display: none; }
  .search__submit-icon { display: block; }
  .search__submit { width: 44px; padding-inline: 0; }
  /* numeral only, but clipped so the accessible name survives */
  .pf__thumb-label span:last-child {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip-path: inset(50%); overflow: hidden; white-space: nowrap;
  }
  .pf__thumb-num { min-width: 0; }
}
