@charset "UTF-8";

/* ==========================================================================
   MỘC — xưởng thiết kế & sản xuất nội thất
   ==========================================================================

   CONCEPT
   Workshop warmth: the page is laid out like an open workshop ledger — a bone
   paper ground, walnut and ink for structure, and a single clay accent used
   only where the hand is being asked to do something. Local photography brings
   the spaces to life; technical joinery
   diagrams remain in the process and materials sections.

   PALETTE (roles + measured contrast, WCAG 2.2)
   --bone      #F3EBDD  page ground / light text on ink
   --bone-2    #E9DDC9  sunken panels, input fill
   --ink       #17120E  primary text, dark surfaces      ink on bone .......... 15.7:1
   --ink-2     #241A13  raised dark surface (footer)     bone on ink .......... 15.7:1
   --ink-soft  #5A4A3A  text on the measure grid         ink-soft on #E2D4BD ... 5.8:1
   --clay      #A75B3C  accent fill, rules, swatches     clay on ink ........... 3.7:1 (non-text + large only)
   --clay-deep #8A4630  accent TEXT on bone              clay-deep on bone ..... 5.9:1
   --clay-light#D08A66  accent text + focus ring on ink  clay-light on ink ..... 6.7:1
   --muted     #6B5B4B  secondary text on bone           muted on bone ......... 5.5:1
                                                         muted on bone-2 ....... 4.9:1
   --muted-2   #C4B49C  secondary text on ink            muted-2 on ink ........ 9.2:1
                                                         muted-2 on ink-2 ...... 8.4:1
   Error text  #8A2F1C  on the input fill                error on bone-2 ....... 6.3:1
   Placeholder #655545  on the input fill                placeholder on bone-2 . 5.3:1

   CONTROL BOUNDARIES (WCAG 2.2 SC 1.4.11 needs 3:1 against the adjacent fill)
   .field__input border  rgba(23,18,14,.55) on bone-2 ... 3.8:1
   .chip / .btn--ghost   rgba(23,18,14,.50) on bone ..... 3.4:1
   The decorative hairlines stay at rgba(23,18,14,.16) — they separate blocks,
   they do not bound a control.

   TYPE
   Display: Fraunces 300–700 (+ italic, optical sizing) — headline voice only,
            never < 20px. Chosen over the more obvious didones because it is
            one of the few display serifs Google serves with a `vietnamese`
            unicode-range subset; without that subset the diacritics decompose.
   Text:    Work Sans 300–700 — everything else (also carries a vietnamese subset).
   h1       clamp(2.5rem, 7.2vw, 5.6rem)  / 0.94  / -0.022em / 400
   h2       clamp(2rem, 4.1vw, 3.3rem)    / 1.02  / -0.015em / 400
   h3       clamp(1.5rem, 2.6vw, 2.05rem) / 1.10  / -0.012em / 400 (serif)
            card + list h3: 1.1875–1.4rem serif; form result 1.6–2.1rem serif
   body     1.0625rem (17px) / 1.66 / 0 / 400
   small    .9375rem (15px) / 1.55 / 0 / 400
   meta     .75rem (12px) / 1.4 / 0.16em uppercase / 600

   LAYOUT
   A 1240px shell with a 5 / 6 asymmetric split used exactly once. The one
   structural idea: a sticky split screen in the process section — media pins
   on the left (5 columns) while three narrative steps scroll past on the right
   (6 columns). Every other section is a single measured column so that split
   screen reads as the page's only "spread".

   SIGNATURE MOTION
   Sticky-media crossfade. Three stacked panels live inside the pinned column.
   Panel 1 is the resting frame; panels 2 and 3 fade and scale in over it as
   their step crosses the viewport, driven by a named `view()` timeline per
   step (`timeline-scope` on the grid). Engines without scroll-driven
   animations fall back to an IntersectionObserver that toggles `.is-active` —
   no scroll listener anywhere on the page.

   PORT
   "Dự án" — photographic interior inspiration with working category filters,
   clearly labelled as stock imagery.
   ========================================================================== */

/* ---------- Tokens ---------- */
:root {
  --bone: #f3ebdd;
  --bone-2: #e9ddc9;
  --ink: #17120e;
  --ink-2: #241a13;
  --clay: #a75b3c;
  --clay-deep: #8a4630;
  --clay-light: #d08a66;
  --muted: #6b5b4b;
  --muted-2: #c4b49c;
  --ink-soft: #5a4a3a;

  --ease-enter: cubic-bezier(.16, 1, .3, 1);
  --ease-expo: cubic-bezier(.19, 1, .22, 1);
  --ease-move: cubic-bezier(.65, 0, .35, 1);
  --ease-exit: cubic-bezier(.7, 0, .84, 0);
  --ease-pop: cubic-bezier(.34, 1.4, .64, 1);
  --dur-micro: 140ms;
  --dur-small: 220ms;
  --dur-med: 320ms;
  --dur-large: 480ms;
  --dur-reveal: 680ms;
  --dur-hero: 900ms;

  --shell: 1240px;
  --gutter: clamp(1.25rem, 4.2vw, 4rem);
  --head-h: 68px;

  /* The local fallbacks are chosen for diacritics, not for style. Georgia — the
     obvious pick after a display serif — decomposes Vietnamese stacked marks on
     Windows ("mô´i nô´i"), so Cambria and Palatino come first; both compose them
     correctly and are present on every Windows install. */
  --font-display: "Fraunces", Cambria, "Palatino Linotype", Georgia, serif;
  --font-text: "Work Sans", "Segoe UI", system-ui, -apple-system, sans-serif;

  --rule: 1px solid rgba(23, 18, 14, .16);
  --rule-light: 1px solid rgba(243, 235, 221, .18);
}

/* ---------- Reset ---------- */
*,
*::before,
*::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--head-h) + 24px);
}

body {
  margin: 0;
  background: var(--bone);
  color: var(--ink);
  font-family: var(--font-text);
  font-size: 1.0625rem;
  line-height: 1.66;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: clip;
}

h1, h2, h3, h4, p, ul, ol, dl, dd, figure, blockquote { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img, svg { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }
a { color: inherit; }

::selection { background: var(--clay-deep); color: var(--bone); }

:focus-visible {
  outline: 2px solid var(--clay-deep);
  outline-offset: 2px;
  border-radius: 2px;
}

/* Focus rings flip with the surface, not with the section. The booking block is
   ink on the left and a bone card on the right, so scoping the light ring to the
   section would have put a 2.1:1 clay-light ring on the form's own controls —
   the primary conversion path. */
.site-foot :focus-visible,
.booking__intro :focus-visible {
  outline-color: var(--clay-light);
}
.booking__card :focus-visible { outline-color: var(--clay-deep); }

/* ---------- Utilities ---------- */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.skip {
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 200;
  padding: .75rem 1.1rem;
  background: var(--ink);
  color: var(--bone);
  font-size: .9375rem;
  font-weight: 600;
  text-decoration: none;
  border-radius: 3px;
  transform: translateY(-160%);
  transition: transform var(--dur-small) var(--ease-enter);
}
.skip:focus-visible { transform: translateY(0); }

/* The rule is positioned rather than flex-centred on purpose: the hero eyebrow
   wraps to two lines on a narrow phone, and a centred rule would end up floating
   between them. Anchored to the first line it stays put however the text breaks. */
.eyebrow {
  position: relative;
  padding-left: 2.5rem;
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--clay-deep);
}
.eyebrow__rule {
  position: absolute;
  left: 0;
  top: .8em;
  width: 28px;
  height: 1px;
  background: currentColor;
}
/* The hero kicker is the longest eyebrow on the page and outgrows a 360px line by
   about 20px. Tightening the tracking and shortening the rule buys that back, so
   it holds one line instead of orphaning its last word on a third line. */
@media (max-width: 419px) {
  .eyebrow { padding-left: 2rem; letter-spacing: .11em; }
  .eyebrow__rule { width: 22px; }
}
.eyebrow--light { color: var(--clay-light); }

/* ---------- Buttons ---------- */
.btn {
  --btn-bg: transparent;
  --btn-fg: var(--ink);
  --btn-line: var(--ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .6rem;
  min-height: 48px;
  padding: .85rem 1.5rem;
  border: 1px solid var(--btn-line);
  border-radius: 2px;
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-size: .9375rem;
  font-weight: 600;
  letter-spacing: .01em;
  line-height: 1.2;
  text-decoration: none;
  transition:
    background-color var(--dur-small) var(--ease-move),
    color var(--dur-small) var(--ease-move),
    border-color var(--dur-small) var(--ease-move),
    transform var(--dur-micro) var(--ease-move);
}
.btn:active { transform: scale(.97); transition-duration: 90ms; }

.btn--solid { --btn-bg: var(--ink); --btn-fg: var(--bone); --btn-line: var(--ink); }
.btn--ghost { --btn-bg: transparent; --btn-fg: var(--ink); --btn-line: rgba(23,18,14,.5); }
.btn--wide { width: 100%; }

@media (hover: hover) and (pointer: fine) {
  .btn--solid:hover { --btn-bg: var(--clay-deep); --btn-line: var(--clay-deep); }
  .btn--ghost:hover { --btn-line: var(--ink); --btn-bg: rgba(23,18,14,.05); }
}

/* ---------- Header ---------- */
.site-head {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  height: var(--head-h);
  display: flex;
  align-items: center;
}
.site-head::before {
  content: "";
  position: absolute;
  inset: 0;
  /* One alpha, not two. An rgba fill *and* an opacity would multiply to ~.38
     effective coverage and drop the nav's ink text to ~3.2:1 over the booking
     block. The tint lives in `opacity` alone; `is-stuck` raises it to 1. */
  background: var(--bone);
  -webkit-backdrop-filter: blur(14px) saturate(1.15);
  backdrop-filter: blur(14px) saturate(1.15);
  opacity: .62;
  transition: opacity var(--dur-med) var(--ease-move);
  pointer-events: none;
}
.site-head::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 1px;
  background: rgba(23, 18, 14, .18);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-large) var(--ease-expo);
  pointer-events: none;
}
.site-head.is-stuck::before { opacity: 1; }
.site-head.is-stuck::after { transform: scaleX(1); }

@supports (animation-timeline: scroll()) {
  .site-head::before {
    animation: head-solidify linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 240px;
  }
  .site-head::after {
    animation: head-rule linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 240px;
  }
}

/* Fallback for engines without scroll-driven animations: a 1px sentinel at the
   top of the document toggles .is-stuck through IntersectionObserver. */
.head-sentinel {
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}

@keyframes head-solidify {
  from { opacity: .62; }
  to   { opacity: 1; }
}
@keyframes head-rule {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.site-head__inner {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  align-items: center;
  gap: 1.25rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  min-height: 44px;
  text-decoration: none;
  margin-right: auto;
}
.brand__mark { width: 26px; height: 26px; color: var(--ink); flex: none; }
.brand__word {
  font-family: var(--font-display); font-optical-sizing: auto;
  font-size: 1.65rem;
  line-height: 1;
  letter-spacing: -.01em;
}
.brand__sub {
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--muted);
  padding-left: .55rem;
  border-left: 1px solid rgba(23,18,14,.24);
  line-height: 1;
}

.site-nav__list { display: flex; align-items: center; gap: .25rem; }
.site-nav__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  /* The shortest label ("Dự án") lands one pixel under the 44px target on its
     own; the floor keeps every nav item a full-size target. */
  min-width: 44px;
  padding: 0 .85rem;
  font-size: .9375rem;
  font-weight: 500;
  text-decoration: none;
  color: var(--ink);
  position: relative;
}
.site-nav__link::after {
  content: "";
  position: absolute;
  left: .85rem;
  right: .85rem;
  bottom: 10px;
  height: 1px;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: right center;
  transition: transform var(--dur-med) var(--ease-expo);
}
@media (hover: hover) and (pointer: fine) {
  .site-nav__link:hover::after { transform: scaleX(1); transform-origin: left center; }
}
.site-nav__link:focus-visible::after { transform: scaleX(1); transform-origin: left center; }
/* Where the reader currently is — a permanent hairline, same device as hover
   and focus. Deliberately no weight change: that would resize the link and
   jitter its neighbours as the mark moves down the list. */
.site-nav__link[aria-current="page"]::after { transform: scaleX(1); transform-origin: left center; }

.btn--head { min-height: 44px; padding: .6rem 1.15rem; font-size: .875rem; }

.nav-toggle {
  display: none;
  position: relative;
  width: 44px;
  height: 44px;
  margin-right: -.5rem;
  border: 0;
  background: transparent;
  color: var(--ink);
}
.nav-toggle__bar {
  position: absolute;
  left: 11px;
  width: 22px;
  height: 1.5px;
  background: currentColor;
  transition: transform var(--dur-med) var(--ease-move), opacity var(--dur-micro) linear;
}
.nav-toggle__bar:nth-child(1) { top: 18px; }
.nav-toggle__bar:nth-child(2) { top: 25px; }
.site-head.is-open .nav-toggle__bar:nth-child(1) { transform: translateY(3.5px) rotate(45deg); }
.site-head.is-open .nav-toggle__bar:nth-child(2) { transform: translateY(-3.5px) rotate(-45deg); }

/* ---------- Shared section head ---------- */
.section-head {
  display: grid;
  gap: 1.1rem;
  padding-bottom: clamp(2.25rem, 4.5vw, 3.5rem);
  border-bottom: var(--rule);
  margin-bottom: clamp(2rem, 4vw, 3.25rem);
}
.section-head__title {
  font-family: var(--font-display); font-optical-sizing: auto;
  font-weight: 400;
  font-size: clamp(2rem, 4.1vw, 3.3rem);
  line-height: 1.02;
  letter-spacing: -.015em;
  max-width: 20ch;
  text-wrap: balance;
}
.section-head__lede {
  max-width: 52ch;
  color: var(--muted);
  font-size: 1.0625rem;
  line-height: 1.62;
}
@media (min-width: 900px) {
  .section-head--split {
    grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
    gap: clamp(2rem, 6vw, 5rem);
    align-items: end;
  }
  .section-head--split .section-head__lede { padding-bottom: .35rem; }
}

/* ==========================================================================
   HERO
   ========================================================================== */
.hero {
  position: relative;
  padding-top: calc(var(--head-h) + clamp(2.5rem, 7vw, 6rem));
  padding-bottom: clamp(3rem, 6vw, 5.5rem);
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: clip;
}
@supports not (overflow: clip) {
  .hero { overflow: hidden; }
}
html:not(.js) .hero { min-height: auto; }
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(120% 70% at 88% 8%, rgba(167, 91, 60, .13), transparent 62%),
    radial-gradient(90% 60% at 4% 96%, rgba(74, 46, 30, .10), transparent 66%);
  pointer-events: none;
}

.hero__grid {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: grid;
  gap: clamp(2.5rem, 5vw, 4rem);
  align-items: center;
}

.hero__copy { display: grid; gap: clamp(1.15rem, 2vw, 1.6rem); justify-items: start; }

.hero__title {
  font-family: var(--font-display); font-optical-sizing: auto;
  font-weight: 400;
  font-size: clamp(2.5rem, 7.2vw, 5.6rem);
  line-height: .94;
  letter-spacing: -.022em;
  margin-block: .25rem .35rem;
}
.hero__line { display: block; }
.hero__line--last { color: var(--clay-deep); }
.hero__title em { font-style: italic; letter-spacing: -.012em; }
.hero__lede {
  max-width: 46ch;
  font-size: clamp(1.0625rem, 1.35vw, 1.1875rem);
  line-height: 1.62;
  color: var(--muted);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: .75rem;
  margin-top: .4rem;
}

.hero__facts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0;
  width: 100%;
  margin-top: clamp(1rem, 2.5vw, 1.75rem);
  border-top: var(--rule);
  padding-top: 1.1rem;
}
.hero__fact { padding-right: 1rem; }
.hero__fact + .hero__fact { padding-left: 1rem; border-left: var(--rule); }
.hero__fact dt {
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--clay-deep);
  margin-bottom: .3rem;
}
.hero__fact dd { font-size: .875rem; line-height: 1.45; color: var(--ink); }

/* Local photography; the process diagrams below remain technical drawings. */
.hero__media { position: relative; padding-bottom: 2.6rem; min-width: 0; }
.grain-stack { position: relative; aspect-ratio: 4 / 5; width: 100%; max-width: 520px; margin-left: auto; }
.grain-stack__photo { width: 100%; height: 100%; border-radius: 2px; }
.grain-stack__caption { position: absolute; left: 0; bottom: -2.1rem; font-size: .75rem; font-weight: 600; letter-spacing: .09em; color: var(--muted); }

.hero__scroll {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: .65rem;
  align-self: flex-start;
  margin: clamp(2rem, 4vw, 3rem) auto 0;
  padding: .5rem .25rem;
  min-height: 44px;
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .15em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--muted);
}
.hero__scroll svg { width: 10px; height: 18px; }
.hero__scroll svg path {
  animation: nudge 2.6s var(--ease-move) infinite;
}
@keyframes nudge {
  0%, 62%, 100% { transform: translateY(0); opacity: .55; }
  18% { transform: translateY(4px); opacity: 1; }
  40% { transform: translateY(0); opacity: .55; }
}

@media (min-width: 880px) {
  .hero__grid {
    grid-template-columns: minmax(0, 1.18fr) minmax(0, .82fr);
    gap: clamp(2rem, 5vw, 4.5rem);
  }
  .hero__scroll { align-self: center; }
}
@media (max-width: 879px) {
  .hero { min-height: auto; }
  .grain-stack { max-width: 100%; aspect-ratio: 5 / 4; margin-left: 0; }
  /* The caption stays absolutely positioned below the stack, as on desktop. Left
     in flow it would sit at the top of the aspect-ratio box — the box has no
     other in-flow children — and print across the walnut pane. The padding is
     what keeps it clear of the scroll hint underneath. */
  .hero__media { padding-bottom: 2.6rem; }
  .hero__facts { grid-template-columns: 1fr; gap: .9rem; }
  .hero__fact { padding: 0; }
  .hero__fact + .hero__fact { padding-left: 0; border-left: 0; border-top: var(--rule); padding-top: .9rem; }
}

/* ==========================================================================
   PROCESS — sticky split screen (signature motion)
   ========================================================================== */
.process {
  position: relative;
  padding-block: clamp(3.5rem, 8vw, 7rem);
  background:
    linear-gradient(180deg, rgba(233, 221, 201, .55), rgba(243, 235, 221, 0) 22%);
  border-top: var(--rule);
}

.process__grid {
  display: grid;
  gap: clamp(2rem, 4vw, 3.5rem);
  align-items: start;
}

/* --- pinned media column --- */
.process__media {
  position: relative;
  aspect-ratio: 4 / 5;
  border-radius: 2px;
  overflow: hidden;
  background: var(--bone-2);
  box-shadow: 0 30px 70px -46px rgba(23, 18, 14, .6);
  contain: paint;
}

.panel {
  position: absolute;
  inset: 0;
  scale: 1;
}
.panel--2 { z-index: 2; }
.panel--3 { z-index: 3; }

/* Without JS only the first panel is shown; the other two restate copy that
   already lives in the step list, so nothing is lost. */
html:not(.js) .panel:not(.panel--1) { display: none; }

/* Panel 1 stays painted; panels 2 and 3 fade in on top of it, so the pinned
   column is never an empty frame and the crossfade reverses cleanly. */
.js .panel--2,
.js .panel--3 {
  opacity: 0;
  scale: .97;
  transition:
    opacity var(--dur-large) var(--ease-move),
    scale var(--dur-reveal) var(--ease-expo);
}
.js .panel--2.is-active,
.js .panel--3.is-active { opacity: 1; scale: 1; }

.panel__surface { position: absolute; inset: 0; }

.panel__surface--measure {
  background-color: #e2d4bd;
  background-image:
    linear-gradient(90deg, rgba(74,46,30,.13) 1px, transparent 1px),
    linear-gradient(0deg, rgba(74,46,30,.13) 1px, transparent 1px),
    radial-gradient(120% 100% at 20% 0%, #f0e5d3, #d8c7ac 80%);
  background-size: 34px 34px, 34px 34px, 100% 100%;
}
.panel__surface--walnut {
  background-color: #4a2e1e;
  background-image:
    repeating-linear-gradient(95deg, rgba(23,18,14,.26) 0 2px, transparent 2px 10px),
    repeating-linear-gradient(92deg, rgba(243,235,221,.08) 0 1px, transparent 1px 30px),
    radial-gradient(110% 80% at 24% 16%, #6b4429, #372116 76%);
}
.panel__surface--ink {
  background-color: #17120e;
  background-image:
    repeating-linear-gradient(0deg, rgba(243,235,221,.05) 0 1px, transparent 1px 18px),
    repeating-linear-gradient(90deg, rgba(243,235,221,.04) 0 1px, transparent 1px 18px),
    radial-gradient(100% 80% at 76% 12%, #2b2019, #100c09 78%);
}

.panel__index {
  position: absolute;
  top: clamp(1rem, 2.4vw, 1.6rem);
  left: clamp(1rem, 2.4vw, 1.6rem);
  font-family: var(--font-display); font-optical-sizing: auto;
  font-size: 1rem;
  letter-spacing: .06em;
  color: rgba(243, 235, 221, .9);
}
.panel--1 .panel__index { color: var(--ink); }

.panel__drawing {
  position: absolute;
  inset: 16% 12% 20%;
  width: auto;
  height: auto;
  color: rgba(243, 235, 221, .8);
}
.panel--1 .panel__drawing { color: rgba(23, 18, 14, .68); }

/* Panel 1 is the resting state, so its diagram is always drawn. Only the
   structural paths are masked; `.guide` dimension lines keep the dashes they
   declare in the markup, which is what makes the drawing read as a drawing. */

.panel--2 .drawing-stroke path:not(.guide),
.panel--2 .drawing-stroke rect:not(.guide),
.panel--3 .drawing-stroke path:not(.guide),
.panel--3 .drawing-stroke rect:not(.guide) {
  stroke-dasharray: 1400;
  stroke-dashoffset: 1400;
  transition: stroke-dashoffset 1.5s var(--ease-expo) .1s;
}
.panel--2.is-active .drawing-stroke path:not(.guide),
.panel--2.is-active .drawing-stroke rect:not(.guide),
.panel--3.is-active .drawing-stroke path:not(.guide),
.panel--3.is-active .drawing-stroke rect:not(.guide) { stroke-dashoffset: 0; }

.panel--2 .drawing-fill,
.panel--2 .drawing-dot,
.panel--3 .drawing-fill,
.panel--3 .drawing-dot {
  opacity: 0;
  transition: opacity var(--dur-med) var(--ease-move) .5s;
}
.panel--2.is-active .drawing-fill,
.panel--2.is-active .drawing-dot,
.panel--3.is-active .drawing-fill,
.panel--3.is-active .drawing-dot { opacity: 1; }

.panel__caption {
  position: absolute;
  inset: auto clamp(1rem, 2.4vw, 1.6rem) clamp(1rem, 2.4vw, 1.6rem);
  font-size: .8125rem;
  line-height: 1.5;
  color: rgba(243, 235, 221, .9);
  max-width: 32ch;
  text-wrap: pretty;
}
.panel--1 .panel__caption { color: var(--ink-soft); }

/* --- narrative column --- */
.process__steps {
  display: grid;
  gap: clamp(2rem, 4vw, 3rem);
}
.step {
  display: grid;
  gap: .8rem;
  padding-top: 1.4rem;
  border-top: var(--rule);
  align-content: start;
}
.step__num {
  display: flex;
  align-items: center;
  gap: .7rem;
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--clay-deep);
}
.step__num::before {
  content: "";
  width: 18px;
  height: 1px;
  background: currentColor;
}
.step__title {
  font-family: var(--font-display); font-optical-sizing: auto;
  font-weight: 400;
  font-size: clamp(1.5rem, 2.6vw, 2.05rem);
  line-height: 1.1;
  letter-spacing: -.012em;
  text-wrap: balance;
}
.step__body {
  color: var(--muted);
  font-size: 1rem;
  line-height: 1.66;
  max-width: 48ch;
}
.step__list {
  display: grid;
  gap: .45rem;
  margin-top: .2rem;
}
.step__list li {
  position: relative;
  padding-left: 1.35rem;
  font-size: .9375rem;
  line-height: 1.55;
  color: var(--ink);
}
.step__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: .58em;
  width: 8px;
  height: 1px;
  background: var(--clay);
}

/* Mobile: one panel stays pinned above the step list while the steps scroll
   underneath it — the same split-screen idea folded into one column. The grid
   becomes a plain block here so the sticky child shares one containing block
   with the whole step list (a single-column grid would trap it in row one). */
@media (max-width: 899px) {
  .process__grid { display: block; }
  .process__media {
    position: sticky;
    /* Flush with the header, not 10px below it: a gap here leaves a strip of
       scrolling step text flashing between the glass bar and the pinned frame. */
    top: var(--head-h);
    z-index: 2;
    aspect-ratio: 3 / 2;
    margin-bottom: 1.75rem;
    box-shadow: 0 18px 40px -26px rgba(23, 18, 14, .55);
  }
  .step { padding-top: 1.1rem; min-height: 46vh; align-content: center; }
  /* Explicit width/height here, not `inset` + auto: as a replaced element the
     SVG would take its height from the viewBox ratio and run under the
     caption. A real box letterboxes it (preserveAspectRatio) instead. */
  .panel__drawing { inset: 12% auto auto 26%; width: 48%; height: 56%; }
  .panel__caption { font-size: .75rem; max-width: 22ch; }
  .panel__index { top: .9rem; left: .95rem; font-size: .875rem; }
}

@media (min-width: 900px) {
  .process__grid {
    grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
    column-gap: clamp(2.5rem, 6vw, 6rem);
  }
  .process__media {
    position: sticky;
    top: calc(var(--head-h) + clamp(1.5rem, 4vh, 3rem));
    max-height: calc(100svh - var(--head-h) - 4rem);
  }
  .process__steps { gap: 0; }
  /* The rule is a flex child, not a border. As a border on a centred flex
     column it painted at the top of a ~740px box while the text sat in the
     middle, leaving a floating hairline above a large void. */
  .step {
    min-height: 64vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding-block: 2rem;
    border-top: 0;
  }
  .step::before {
    content: "";
    display: block;
    flex: none;
    height: 1px;
    background: rgba(23, 18, 14, .16);
    margin-bottom: 1.4rem;
  }
  .step:first-child { min-height: 60vh; padding-top: 0; }
  .step:last-child { min-height: 56vh; }
  /* The section head above already closes with a rule; a second one 2rem
     lower is noise, not structure. */
  .step:first-child::before { content: none; }
}

/* Below 900px the panels sit above the steps, so a single frame is enough —
   the caption carries the state, and the observer swaps it. */

/* --- scroll-driven crossfade (progressive enhancement) ----------------
   Each step owns a named view() timeline. `timeline-scope` on the grid makes
   those names visible to the panels, which live in the pinned column and
   therefore cannot have a useful view() timeline of their own. Panel 1 is the
   resting frame; panels 2 and 3 fade in over it as their step enters, and
   fade back out on the way up, so the pinned column is never blank.          */
@supports (animation-timeline: view()) and (timeline-scope: --x) {
  @media (min-width: 900px) and (prefers-reduced-motion: no-preference) {
    .process__grid {
      timeline-scope: --step-2, --step-3;
    }
    .step[data-step="2"] { view-timeline-name: --step-2; }
    .step[data-step="3"] { view-timeline-name: --step-3; }

    .js .panel--2,
    .js .panel--3 {
      transition: none;
      animation: panel-in linear both;
      /* Each step is ~64vh tall inside a 100vh viewport, so the window from
         "heading entering the lower third" to "heading settled at reading
         height" is roughly entry 45% → entry 82%. Fading across exactly that
         window means the pinned frame is always one diagram at full strength,
         never two at half opacity. */
      animation-range: entry 45% entry 82%;
    }
    .js .panel--2 { animation-timeline: --step-2; }
    .js .panel--3 { animation-timeline: --step-3; }

    /* `animation` is a shorthand and resets every animation-* longhand,
       including animation-timeline. The timeline therefore has to be declared
       in the same block, after the shorthand — declare it before, or in an
       earlier rule, and it is silently thrown away, leaving the stroke on the
       document timeline at 0s so it snaps to its end state at load. */
    .js .panel--2 .drawing-stroke path:not(.guide),
    .js .panel--2 .drawing-stroke rect:not(.guide) {
      stroke-dashoffset: 1400;
      transition: none;
      animation: draw-in linear both;
      animation-timeline: --step-2;
      animation-range: entry 30% entry 120%;
    }
    .js .panel--3 .drawing-stroke path:not(.guide),
    .js .panel--3 .drawing-stroke rect:not(.guide) {
      stroke-dashoffset: 1400;
      transition: none;
      animation: draw-in linear both;
      animation-timeline: --step-3;
      animation-range: entry 30% entry 120%;
    }

    .js .panel--2 .drawing-fill,
    .js .panel--2 .drawing-dot {
      opacity: 0;
      transition: none;
      animation: fade-in linear both;
      animation-timeline: --step-2;
      animation-range: entry 45% entry 115%;
    }
    .js .panel--3 .drawing-fill,
    .js .panel--3 .drawing-dot {
      opacity: 0;
      transition: none;
      animation: fade-in linear both;
      animation-timeline: --step-3;
      animation-range: entry 45% entry 115%;
    }
  }
}

@keyframes panel-in {
  from { opacity: 0; scale: .955; }
  to   { opacity: 1; scale: 1; }
}
@keyframes draw-in {
  from { stroke-dashoffset: 1400; }
  to   { stroke-dashoffset: 0; }
}
@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ==========================================================================
   MATERIALS
   ========================================================================== */
.materials {
  padding-block: clamp(3.5rem, 8vw, 7rem);
  border-top: var(--rule);
  background: linear-gradient(180deg, rgba(233, 221, 201, .38), rgba(243, 235, 221, 0) 30%);
}

/* Sections below the first screen are skipped until they approach the viewport.
   `auto` on the intrinsic size makes the browser remember the measured height,
   so an anchor jump does not land short the second time round. Deliberately not
   applied to .process: its sticky column and its view() timelines both depend on
   the section staying in layout. */
.materials,
.projects {
  content-visibility: auto;
  contain-intrinsic-size: auto 1400px;
}

.wood-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: 0;
  border-top: var(--rule);
}
.wood {
  display: grid;
  align-content: start;
  gap: .55rem;
  padding: clamp(1.25rem, 2.4vw, 1.75rem) clamp(1rem, 2vw, 1.5rem) clamp(1.5rem, 2.6vw, 2rem);
  border-bottom: var(--rule);
}
.wood + .wood { border-left: var(--rule); }
@media (max-width: 1079px) {
  .wood-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .wood:nth-child(odd) { border-left: 0; }
}
@media (max-width: 559px) {
  .wood-list { grid-template-columns: 1fr; }
  .wood + .wood { border-left: 0; }
}

.wood__chip {
  display: block;
  width: 100%;
  height: 56px;
  border-radius: 1px;
  margin-bottom: .65rem;
  box-shadow: inset 0 0 0 1px rgba(23, 18, 14, .14);
}
.wood__chip--walnut {
  background-color: #4a2e1e;
  background-image:
    repeating-linear-gradient(97deg, rgba(23,18,14,.26) 0 2px, transparent 2px 8px),
    radial-gradient(100% 100% at 18% 20%, #6b4429, #38231a 80%);
}
.wood__chip--oak {
  background-color: #c9ab7f;
  background-image:
    repeating-linear-gradient(89deg, rgba(74,46,30,.18) 0 1.5px, transparent 1.5px 7px),
    radial-gradient(100% 100% at 70% 30%, #ddc3a0, #b3915f 82%);
}
.wood__chip--ash {
  background-color: #d8c6ab;
  background-image:
    repeating-linear-gradient(90deg, rgba(74,46,30,.12) 0 1px, transparent 1px 5px),
    radial-gradient(100% 100% at 30% 70%, #e8dcc6, #c3ab8b 84%);
}
.wood__chip--ply {
  background-color: #b99a72;
  background-image:
    repeating-linear-gradient(0deg, rgba(23,18,14,.20) 0 2px, transparent 2px 11px),
    radial-gradient(100% 100% at 80% 10%, #cdb08a, #9d7f5b 86%);
}

.wood__name {
  font-family: var(--font-display); font-optical-sizing: auto;
  font-weight: 400;
  font-size: 1.4rem;
  line-height: 1.15;
  letter-spacing: -.01em;
}
.wood__use {
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--clay-deep);
  text-wrap: balance;
}
/* The longest "where it goes" label wraps to two lines at four-up; reserving
   the second line keeps the four notes starting on the same baseline. `2lh`
   rather than an em multiple, so it tracks the line-height instead of drifting
   when the type token changes. */
@media (min-width: 560px) {
  .wood__use { min-height: 3.35em; min-height: 2lh; }
}
.wood__note {
  font-size: .9375rem;
  line-height: 1.6;
  color: var(--muted);
  margin-top: .15rem;
}
.wood__avoid {
  font-size: .875rem;
  line-height: 1.5;
  color: var(--ink);
  margin-top: .35rem;
  padding-top: .6rem;
  border-top: 1px dashed rgba(23, 18, 14, .2);
}
.wood__avoid span {
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--clay-deep);
  margin-right: .45rem;
}

.finish {
  display: grid;
  gap: clamp(1.25rem, 3vw, 2.5rem);
  align-items: center;
  margin-top: clamp(2.5rem, 5vw, 4rem);
  padding: clamp(1.5rem, 3vw, 2.5rem);
  background: var(--ink);
  color: var(--bone);
  border-radius: 2px;
}
.finish__diagram {
  color: rgba(208, 138, 102, .85);
}
.finish__diagram svg { width: 100%; max-width: 240px; }
.finish__title {
  font-family: var(--font-display); font-optical-sizing: auto;
  font-weight: 400;
  font-size: clamp(1.45rem, 2.6vw, 2rem);
  line-height: 1.12;
  letter-spacing: -.012em;
  margin-bottom: .7rem;
  text-wrap: balance;
}
.finish__copy p {
  color: var(--muted-2);
  font-size: .9375rem;
  line-height: 1.66;
  max-width: 58ch;
}
@media (min-width: 760px) {
  .finish { grid-template-columns: 240px minmax(0, 1fr); }
}

/* ==========================================================================
   PROJECTS — photographic interior inspiration
   ========================================================================== */
.projects {
  padding-block: clamp(3.5rem, 8vw, 7rem);
  border-top: var(--rule);
}

.filters {
  display: grid;
  gap: .75rem;
  margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
}
.filters__label {
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--muted);
}
.filters__bar {
  display: flex;
  gap: .5rem;
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: .25rem;
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
  scroll-padding-inline: var(--gutter);
}
.filters__bar::-webkit-scrollbar { display: none; }

.chip {
  flex: none;
  min-height: 44px;
  padding: .55rem 1.05rem;
  border: 1px solid rgba(23, 18, 14, .5);
  border-radius: 100px;
  background: transparent;
  color: var(--ink);
  font-size: .9375rem;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  transition:
    background-color var(--dur-small) var(--ease-move),
    border-color var(--dur-small) var(--ease-move),
    color var(--dur-small) var(--ease-move),
    transform var(--dur-micro) var(--ease-move);
}
.chip:active { transform: scale(.97); transition-duration: 90ms; }
.chip[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--bone);
}
@media (hover: hover) and (pointer: fine) {
  .chip:not([aria-pressed="true"]):hover { border-color: var(--ink); background: rgba(23, 18, 14, .05); }
}

.filters__count {
  font-size: .8125rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.filters__count span { color: var(--ink); font-weight: 600; }

@media (min-width: 760px) {
  .filters {
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: .6rem 1.5rem;
  }
  .filters__label { grid-column: 1; grid-row: 1; }
  .filters__bar { grid-column: 1; grid-row: 2; margin-inline: 0; padding-inline: 0; }
  .filters__count { grid-column: 2; grid-row: 1 / span 2; align-self: center; }
}

.project-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 264px), 1fr));
  gap: clamp(1rem, 2vw, 1.5rem);
}
/* Four columns at desktop: 8 cards fill two clean rows. */
@media (min-width: 1080px) {
  .project-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
.project-grid[hidden] { display: none; }

.project {
  display: grid;
  align-content: start;
  gap: .2rem;
}
.project[hidden] { display: none; }

.project__panel {
  position: relative;
  aspect-ratio: 5 / 4;
  border-radius: 2px;
  overflow: hidden;
  margin-bottom: .85rem;
  isolation: isolate;
}
.project__panel::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(23, 18, 14, 0) 52%, rgba(23, 18, 14, .42));
  opacity: .55;
  transition: opacity var(--dur-med) var(--ease-move);
  pointer-events: none;
}

.project__tag {
  position: absolute;
  left: .9rem;
  bottom: .8rem;
  z-index: 2;
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .15em;
  text-transform: uppercase;
  color: var(--bone);
}

.project__title {
  font-family: var(--font-display); font-optical-sizing: auto;
  font-weight: 400;
  font-size: 1.1875rem;
  line-height: 1.22;
  letter-spacing: -.008em;
}
/* Titles run one or two lines depending on the name. Reserving two lines keeps
   the material notes on a shared baseline across a row instead of stepping. */
@media (min-width: 760px) {
  .project__title { min-height: 2.44em; }
}
.project__meta {
  font-size: .8125rem;
  line-height: 1.5;
  color: var(--muted);
}

@media (hover: hover) and (pointer: fine) {
  .project:hover .project__panel::after { opacity: .85; }
}
/* No lift on hover or focus: the cards hold no link, so a rise would promise a
   click target that does not exist. The photo scrim shifts instead —
   a material response, not an affordance. */

.empty {
  display: grid;
  justify-items: start;
  gap: .7rem;
  padding: clamp(1.75rem, 4vw, 3rem);
  border: 1px dashed rgba(23, 18, 14, .3);
  border-radius: 2px;
  margin-top: .5rem;
}
.empty[hidden] { display: none; }
.empty__title {
  font-family: var(--font-display); font-optical-sizing: auto;
  font-size: 1.5rem;
  line-height: 1.2;
}
.empty__body { color: var(--muted); font-size: .9375rem; max-width: 46ch; }

/* ==========================================================================
   BOOKING
   ========================================================================== */
.booking {
  padding-block: clamp(3.5rem, 8vw, 7rem);
  background: var(--ink);
  color: var(--bone);
  background-image:
    radial-gradient(90% 60% at 82% 0%, rgba(167, 91, 60, .18), transparent 62%),
    radial-gradient(70% 50% at 0% 100%, rgba(107, 68, 41, .26), transparent 70%);
}
.booking__grid {
  display: grid;
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
}
.booking__intro { display: grid; gap: 1.15rem; justify-items: start; }
.booking__title {
  font-family: var(--font-display); font-optical-sizing: auto;
  font-weight: 400;
  font-size: clamp(2.1rem, 4.4vw, 3.5rem);
  line-height: 1.0;
  letter-spacing: -.018em;
  max-width: 16ch;
  text-wrap: balance;
}
.booking__lede {
  color: var(--muted-2);
  font-size: 1.0625rem;
  line-height: 1.64;
  max-width: 48ch;
}
.booking__list { display: grid; gap: .55rem; margin-top: .3rem; }
.booking__list li {
  display: grid;
  grid-template-columns: 1.2rem 1fr;
  font-size: .9375rem;
  line-height: 1.55;
  color: var(--bone);
}
.booking__list li span { color: var(--clay-light); }
.booking__note {
  font-size: .8125rem;
  line-height: 1.55;
  color: var(--muted-2);
  max-width: 44ch;
  padding-top: 1rem;
  border-top: var(--rule-light);
  margin-top: .5rem;
}

.booking__card {
  background: var(--bone);
  color: var(--ink);
  padding: clamp(1.35rem, 3vw, 2.25rem);
  border-radius: 3px;
  box-shadow: 0 40px 90px -50px rgba(0, 0, 0, .8);
}

.form { display: grid; gap: 1.1rem; }
.form[hidden] { display: none; }
.form__lead {
  font-size: .9375rem;
  line-height: 1.5;
  color: var(--muted);
  padding-bottom: .9rem;
  border-bottom: var(--rule);
  margin-bottom: .15rem;
}
.field { display: grid; gap: .4rem; }
.field__label {
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink);
}
.field__hint {
  font-weight: 500;
  letter-spacing: .06em;
  color: var(--muted);
}
.field__input {
  width: 100%;
  min-height: 48px;
  padding: .7rem .85rem;
  /* The fill is only 1.13:1 against the page, so this border is the control's
     entire boundary and has to clear 3:1 on its own (SC 1.4.11). */
  border: 1px solid rgba(23, 18, 14, .55);
  border-radius: 2px;
  background: var(--bone-2);
  color: var(--ink);
  font-size: 1rem;
  line-height: 1.4;
  transition: border-color var(--dur-small) var(--ease-move), background-color var(--dur-small) var(--ease-move);
}
.field__input::placeholder { color: #655545; opacity: 1; }
.field__input:hover { border-color: var(--ink); }
.field__input:focus-visible { outline-offset: 2px; border-color: var(--clay-deep); }
.field__input[aria-invalid="true"] {
  border-color: #8a2f1c;
  background: #f6e3dc;
}
.field__select { position: relative; }
.field__select select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 2.5rem;
  cursor: pointer;
}
.field__select svg {
  position: absolute;
  right: .95rem;
  top: 50%;
  width: 12px;
  height: 8px;
  translate: 0 -50%;
  color: var(--ink);
  pointer-events: none;
}
.field__error {
  font-size: .8125rem;
  line-height: 1.45;
  color: #8a2f1c;
  display: flex;
  gap: .35rem;
  align-items: flex-start;
}
.field__error::before {
  content: "!";
  flex: none;
  display: grid;
  place-items: center;
  width: 16px;
  height: 16px;
  margin-top: .12rem;
  border-radius: 50%;
  background: #8a2f1c;
  color: var(--bone);
  font-size: .75rem;
  font-weight: 700;
  line-height: 1;
}
.field__error[hidden] { display: none; }

.form__foot {
  font-size: .75rem;
  line-height: 1.5;
  color: var(--muted);
  text-align: center;
}

.form-result { display: grid; gap: .9rem; justify-items: start; }
.form-result[hidden] { display: none; }
.form-result__eyebrow {
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--clay-deep);
}
.form-result__title {
  font-family: var(--font-display); font-optical-sizing: auto;
  font-weight: 400;
  font-size: clamp(1.6rem, 3vw, 2.1rem);
  line-height: 1.08;
  letter-spacing: -.012em;
}
.form-result__list {
  width: 100%;
  display: grid;
  gap: .5rem;
  padding: .9rem 0;
  border-top: var(--rule);
  border-bottom: var(--rule);
}
.form-result__list > div {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  font-size: .9375rem;
}
.form-result__list dt { color: var(--muted); }
.form-result__list dd { font-weight: 600; text-align: right; }
.form-result__body { font-size: .875rem; line-height: 1.6; color: var(--muted); }

@media (min-width: 900px) {
  .booking__grid {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
    align-items: center;
  }
  .booking__card { position: sticky; top: calc(var(--head-h) + 2rem); }
}

/* ==========================================================================
   FOOTER
   ========================================================================== */
.site-foot {
  background: var(--ink-2);
  color: var(--bone);
  padding-block: clamp(2.5rem, 5vw, 4rem) 1.5rem;
  border-top: 1px solid rgba(243, 235, 221, .12);
}
.site-foot__top {
  display: grid;
  gap: clamp(1.75rem, 4vw, 3rem);
  padding-bottom: clamp(2rem, 4vw, 3rem);
}
.site-foot__word {
  font-family: var(--font-display); font-optical-sizing: auto;
  font-size: clamp(2.5rem, 6vw, 3.75rem);
  line-height: 1;
  letter-spacing: -.02em;
  margin-bottom: .6rem;
}
.site-foot__desc {
  color: var(--muted-2);
  font-size: .9375rem;
  line-height: 1.6;
  max-width: 34ch;
}
.site-foot__head {
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--clay-light);
  margin-bottom: .75rem;
}
.site-foot__nav ul { display: grid; gap: .1rem; }
.site-foot__nav a,
.site-foot__base a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  /* "Dự án" is the shortest label in this stack and lands just under the 44px
     floor on its own. */
  min-width: 44px;
  font-size: .9375rem;
  color: var(--bone);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-small) var(--ease-move), color var(--dur-small) var(--ease-move);
}
@media (hover: hover) and (pointer: fine) {
  .site-foot__nav a:hover,
  .site-foot__base a:hover { border-bottom-color: var(--clay-light); }
}
.site-foot__meta p {
  color: var(--muted-2);
  font-size: .875rem;
  line-height: 1.6;
  max-width: 40ch;
}
.site-foot__meta a {
  display: inline;
  min-height: 0;
  color: var(--bone);
  border-bottom: 1px solid rgba(243, 235, 221, .4);
}
.site-foot__base {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.5rem;
  align-items: center;
  justify-content: space-between;
  padding-top: 1.25rem;
  border-top: 1px solid rgba(243, 235, 221, .14);
  font-size: .75rem;
  color: var(--muted-2);
}
.site-foot__base a { font-size: .75rem; min-height: 44px; color: var(--muted-2); }

@media (min-width: 800px) {
  .site-foot__top {
    grid-template-columns: minmax(0, 1.4fr) minmax(0, .8fr) minmax(0, 1.1fr);
    gap: clamp(2rem, 5vw, 4rem);
  }
}

/* ==========================================================================
   REVEAL (JS enhancement only)
   ========================================================================== */
html.js-motion [data-reveal] {
  opacity: 0;
  translate: 0 18px;
  transition:
    opacity var(--dur-reveal) var(--ease-enter),
    translate var(--dur-reveal) var(--ease-enter),
    scale var(--dur-med) var(--ease-enter);
}
html.js-motion [data-reveal].is-revealed {
  opacity: 1;
  translate: none;
}
/* Filter exit. Must sit after the reveal block and match its specificity, or
   `.is-revealed` keeps opacity at 1 and the "fade" is a 1.5% shrink followed
   by a hard `hidden` snap. The exit timing lives here too: the reveal rule
   owns the transition on a revealed card, so the faster exit has to restate
   it rather than rely on `.project`. */
html.js-motion [data-reveal].is-hiding {
  opacity: 0;
  translate: 0 6px;
  scale: .985;
  transition:
    opacity var(--dur-small) var(--ease-exit),
    translate var(--dur-small) var(--ease-exit),
    scale var(--dur-small) var(--ease-exit);
}

/* Hero: LCP-safe mask reveal — the headline stays painted, it only slides */
html.js-motion .hero__line {
  display: block;
  overflow: hidden;
  padding-bottom: .06em;
}
html.js-motion .hero__line-in {
  display: block;
  animation: line-up var(--dur-hero) var(--ease-expo) both;
}
html.js-motion .hero__line:nth-child(1) .hero__line-in { animation-delay: 80ms; }
html.js-motion .hero__line:nth-child(2) .hero__line-in { animation-delay: 150ms; }
html.js-motion .hero__line:nth-child(3) .hero__line-in { animation-delay: 220ms; }
html.js-motion .hero__lede { animation: rise-in var(--dur-reveal) var(--ease-expo) 340ms both; }
html.js-motion .hero__actions { animation: rise-in var(--dur-reveal) var(--ease-expo) 420ms both; }
html.js-motion .hero__facts { animation: rise-in var(--dur-reveal) var(--ease-expo) 500ms both; }
html.js-motion .hero .eyebrow { animation: rise-in var(--dur-reveal) var(--ease-expo) 40ms both; }
html.js-motion .grain-stack__caption { animation: fade-soft var(--dur-reveal) var(--ease-enter) 520ms both; }

@keyframes line-up {
  from { transform: translateY(102%); }
  to   { transform: translateY(0); }
}
@keyframes rise-in {
  from { transform: translateY(14px); opacity: .001; }
  to   { transform: translateY(0); opacity: 1; }
}
@keyframes pane-in {
  from { transform: translateY(22px) scale(.985); opacity: .001; }
  to   { transform: translateY(0) scale(1); opacity: 1; }
}
@keyframes fade-soft {
  from { opacity: .001; }
  to   { opacity: 1; }
}

/* ==========================================================================
   MOBILE NAV
   The collapsed drawer is a JS-only affordance: without JS the header becomes
   a static bar and the links are always visible, so navigation never depends
   on a script that might not load.
   ========================================================================== */
html:not(.js) .site-head {
  position: static;
  height: auto;
  padding-block: .35rem;
}
html:not(.js) .site-head::before { background: var(--bone); opacity: 1; -webkit-backdrop-filter: none; backdrop-filter: none; }
html:not(.js) .site-head::after { display: none; }
html:not(.js) .site-head__inner { flex-wrap: wrap; row-gap: .25rem; }
html:not(.js) .nav-toggle { display: none; }

/* Without JS the chips cannot filter anything, so the whole control — chips,
   label and live count — is not shown. A row of buttons that silently does
   nothing is worse than no row; the grid below already lists every project. */
html:not(.js) .filters { display: none; }

@media (max-width: 899px) {
  html.js .nav-toggle { display: block; }
  .btn--head { display: none; }
  .brand__sub { display: none; }

  html.js .site-nav {
    position: fixed;
    inset: var(--head-h) 0 auto 0;
    padding: 1rem var(--gutter) 1.75rem;
    background: var(--bone);
    border-bottom: var(--rule);
    box-shadow: 0 24px 40px -30px rgba(23, 18, 14, .55);
    opacity: 0;
    translate: 0 -10px;
    visibility: hidden;
    /* A short landscape phone must still reach the last link. */
    max-height: calc(100dvh - var(--head-h));
    overflow-y: auto;
    overscroll-behavior: contain;
    transition:
      opacity var(--dur-med) var(--ease-move),
      translate var(--dur-med) var(--ease-exit),
      visibility 0s linear var(--dur-med);
  }
  html.js .site-head.is-open .site-nav {
    opacity: 1;
    translate: 0 0;
    visibility: visible;
    transition:
      opacity var(--dur-small) var(--ease-enter),
      translate var(--dur-large) var(--ease-expo),
      visibility 0s;
  }
  .site-nav__list { flex-direction: column; align-items: stretch; gap: 0; }
  .site-nav__link {
    min-height: 56px;
    padding-inline: 0;
    border-bottom: var(--rule);
    font-size: 1.0625rem;
    justify-content: space-between;
  }
  .site-nav__link::after { display: none; }
  .site-nav__link[aria-current="page"] { color: var(--clay-deep); }
  .site-nav__link::before {
    content: "→";
    order: 2;
    color: var(--clay-deep);
    font-size: .9375rem;
  }
  .site-nav__list li:last-child .site-nav__link { border-bottom: 0; }

  html:not(.js) .site-nav { margin-top: .25rem; }
  html:not(.js) .site-nav__list { flex-direction: row; flex-wrap: wrap; }
  html:not(.js) .site-nav__link { min-height: 44px; padding-inline: .75rem 0; border-bottom: 0; font-size: .9375rem; }
  html:not(.js) .site-nav__link::before { content: none; }
}

@media (min-width: 900px) {
  .site-nav { margin-right: 1.25rem; }
}

/* Solid fallbacks for glass */
@media (prefers-reduced-transparency: reduce) {
  .site-head::before,
  .site-head.is-stuck::before {
    background: var(--bone);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    opacity: 1;
  }
}
@media (prefers-contrast: more) {
  :root {
    --muted: #4a3b2d;
    --muted-2: #e2d6c3;
    --rule: 1px solid rgba(23, 18, 14, .45);
    --rule-light: 1px solid rgba(243, 235, 221, .45);
  }
  .site-head::before { background: var(--bone); -webkit-backdrop-filter: none; backdrop-filter: none; opacity: 1; }
  .btn--ghost { --btn-line: var(--ink); }
  .chip { border-color: rgba(23, 18, 14, .6); }
  .field__input { border-color: var(--ink); }
  .project__panel::after { opacity: .75; }
}

/* ==========================================================================
   REDUCED MOTION
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  html.js-motion [data-reveal] { opacity: 1 !important; translate: none !important; scale: none !important; }
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
  html { scroll-behavior: auto; }
  .js .panel--2,
  .js .panel--3 {
    transition: none;
    animation: none !important;
    scale: 1;
  }
  .js .panel--2 { opacity: 0; }
  .js .panel--3 { opacity: 0; }
  .js .panel--2.is-active,
  .js .panel--3.is-active { opacity: 1; }
  .drawing-stroke path, .drawing-stroke rect { stroke-dashoffset: 0 !important; transition: none !important; }
  .drawing-fill, .drawing-dot { opacity: 1 !important; transition: none !important; }
  /* The filter still has to remove the card; it just does so without travel.
     Zeroing opacity here would leave the card painted for 240 ms after the
     reader has already asked for it to go. */
  html.js-motion [data-reveal].is-hiding {
    translate: none;
    scale: none;
    transition: opacity 120ms linear;
  }
  .hero__scroll svg path { animation: none !important; opacity: 1; }
}



/* Photographic media retain the original card geometry and category controls. */
.photo-real { display: block; max-width: 100%; object-fit: cover; }
.project__photo { position: absolute; inset: 0; width: 100%; height: 100%; }
.project__tag { padding: .3rem .5rem; background: var(--ink); color: var(--bone); }
.photo-note { margin: -1rem 0 1.5rem; max-width: 65ch; color: var(--muted); font-size: .8125rem; line-height: 1.6; }
