/* ============================================================================
   ESTOIKO
   Theme + page classes. The mechanism lives in scrollcraft.css and is not
   restyled here; every selector below is this page's own.

   The palette is not invented. Every ground value comes from a per-frame
   measurement of the client's own footage (design/light-lut.json), and during
   the film the ground and ink are written live from that table by estoiko.js.
   The values below are the resting state and the post-film lock.
   ========================================================================== */

@font-face {
  font-family: "Bodoni Moda";
  src: url("fonts/bodoni-latin.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+2074, U+20AC, U+2122, U+2212;
}
@font-face {
  font-family: "Archivo";
  src: url("fonts/archivo-latin.woff2") format("woff2");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+2074, U+20AC, U+2122, U+2212;
}

:root {
  /* Six roles, one accent hue. The accent carries two lightness stops because
     this page travels between a near-white and a near-black ground and no
     single stop clears 4.5:1 on both. Same hue (205deg) at both stops. */
  --sc-canvas:     #f1f0ef;
  --sc-surface:    #e4e2df;
  --sc-ink:        #161515;
  --sc-ink-soft:   #5d5a56;
  --sc-accent:     #1a5b85;
  --sc-accent-ink: #ffffff;

  --sc-font-display: "Bodoni Moda", "Times New Roman", serif;
  --sc-font-text:    "Archivo", system-ui, -apple-system, sans-serif;

  /* One shape system: sharp. Nothing on this page is rounded. */
  --e-radius: 0px;

  --e-hair:  color-mix(in srgb, var(--sc-ink) 18%, transparent);
  --e-hair2: color-mix(in srgb, var(--sc-ink) 34%, transparent);

  /* The wash under film copy. estoiko.js swaps it with the ink. */
  --e-scrim: #efeeec;

  --e-bar-h: 68px;
  --e-ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* Light text on a dark ground needs compensation on three axes: a touch more
   leading, a touch more tracking, one step more weight. estoiko.js sets this
   flag from the same luminance test that flips the ink. */
html[data-ground="dark"] {
  --e-hair:  color-mix(in srgb, var(--sc-ink) 16%, transparent);
  --e-hair2: color-mix(in srgb, var(--sc-ink) 30%, transparent);
}

body {
  background: var(--sc-canvas);
  color: var(--sc-ink);
  /* The ground moves continuously during the film. Nothing else transitions:
     a transition here would fight the per-frame write and lag the picture. */
  overflow-x: hidden;
}

::selection { background: var(--sc-accent); color: var(--sc-accent-ink); }

:where(a, button, input, textarea, select):focus-visible {
  outline: 2px solid var(--sc-accent);
  outline-offset: 3px;
}

/* ---------------------------------------------------------------- type ---- */

.e-display {
  font-family: var(--sc-font-display);
  font-weight: 400;
  font-optical-sizing: auto;
  line-height: 0.94;
  letter-spacing: -0.025em;
  text-wrap: balance;
  margin: 0;
  /* Bodoni's hairlines thin out badly when light type sits on a dark ground.
     One step of weight back is optical correction, not decoration. */
  font-variation-settings: "wght" 400;
}
html[data-ground="dark"] .e-display {
  font-variation-settings: "wght" 440;
  letter-spacing: -0.019em;
  line-height: 0.97;
}

.e-display--xl { font-size: clamp(2.3rem, 5.6vw, 5.6rem); }
.e-display--lg { font-size: clamp(2rem, 4.4vw, 4.1rem); }
.e-display--md { font-size: clamp(1.7rem, 3.1vw, 2.9rem); letter-spacing: -0.018em; line-height: 1.02; }

.e-lede {
  font-size: clamp(1rem, 1.15vw, 1.2rem);
  line-height: 1.55;
  color: var(--sc-ink-soft);
  max-width: 34ch;
  margin: 0;
  text-wrap: pretty;
}
html[data-ground="dark"] .e-lede { line-height: 1.62; letter-spacing: 0.004em; }

.e-body {
  font-size: 1rem;
  line-height: 1.65;
  color: var(--sc-ink-soft);
  max-width: 62ch;
  margin: 0;
  text-wrap: pretty;
}
html[data-ground="dark"] .e-body { line-height: 1.72; letter-spacing: 0.004em; }

/* The one small uppercase label on this page. It names a discipline, which is
   the page's actual thesis, so it carries information rather than decorating a
   heading. Used exactly twice. */
.e-role {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--sc-accent);
  margin: 0 0 1.1rem;
  display: block;
}

/* ---------------------------------------------------------------- chrome -- */

.e-bar {
  position: fixed;
  inset: 0 0 auto;
  height: var(--e-bar-h);
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0 clamp(1rem, 3.4vw, 2.6rem);
  color: var(--sc-ink);
  mix-blend-mode: normal;
}

.e-mark {
  font-family: var(--sc-font-display);
  font-size: 1.06rem;
  font-weight: 500;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  text-decoration: none;
  color: inherit;
  white-space: nowrap;
}

/* One CTA label on the page. Sharp, hairline, no fill until hover. */
.e-cta {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  border: 1px solid var(--e-hair2);
  border-radius: var(--e-radius);
  background: transparent;
  color: var(--sc-ink);
  font-family: var(--sc-font-text);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 0.72rem 1.15rem;
  white-space: nowrap;
  cursor: pointer;
  transition: background 260ms var(--e-ease), color 260ms var(--e-ease),
              border-color 260ms var(--e-ease);
}
.e-cta__arrow { transition: transform 320ms var(--e-ease); }
.e-cta:hover { background: var(--sc-ink); color: var(--sc-canvas); border-color: var(--sc-ink); }
.e-cta:hover .e-cta__arrow { transform: translateX(4px); }
.e-cta:active { transform: translateY(1px); }

.e-cta--solid {
  background: var(--sc-ink);
  color: var(--sc-canvas);
  border-color: var(--sc-ink);
  padding: 0.95rem 1.5rem;
}
.e-cta--solid:hover { background: var(--sc-accent); border-color: var(--sc-accent); color: var(--sc-accent-ink); }

/* The engine's own [data-sc-progress] bar is the only progress signal on the
   page; a filmic one-shot cannot carry a chapter readout. */

/* ------------------------------------------------------------------ film -- */

.film__stage {
  /* position, height and overflow belong to .sc-stage. Only layout is set here,
     and only where the mobile composition needs it. */
  display: block;
}

.film__plate {
  position: absolute;
  inset: 0;
  overflow: hidden;
  /* The last few percent dissolve into the ground. Without it the plate ends on
     a hard horizontal edge as the stage un-pins, which is the one visible cut in
     an otherwise continuous page. Kept short so it never reaches the copy, which
     sits about 10% up from the bottom. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 95%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 95%, transparent 100%);
}

.film__plate > video,
.film__plate > .sc-stage__poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The emblem and the bust sit slightly above centre through most of the
     sequence, so bias the crop up rather than centring it. */
  object-position: 50% 42%;
}

/* Grade the footage toward the page ground so the film and the page read as
   one atmosphere rather than a video sitting in a hole. */
.film__plate::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--sc-canvas);
  opacity: 0.1;
  mix-blend-mode: soft-light;
  pointer-events: none;
}

.film__copy {
  position: absolute;
  z-index: 3;
  left: clamp(1rem, 3.4vw, 2.6rem);
  right: clamp(1rem, 3.4vw, 2.6rem);
  bottom: clamp(2.6rem, 8vh, 6rem);
  pointer-events: none;
  display: grid;
  color: var(--sc-ink);
  max-height: calc(100vh - var(--e-bar-h) - 8rem);
  /* The ink switches once on the way into the lab and once on the way out of
     the burst. A short dissolve stops that reading as a pop; it is deliberately
     shorter than the time the picture takes to go black or white behind it. */
  transition: color 180ms linear;
}
.film__copy a, .film__copy button { pointer-events: auto; }

/* Every beat occupies the same box so one can leave while the next arrives and
   the frame never shows empty space. */
.beat {
  grid-area: 1 / 1;
  /* Every beat sits on the same baseline. Without this a short beat renders at
     the top of a cell sized by the tallest one, which pushes it off screen. */
  align-self: end;
  /* Not ch: that unit resolves against this element's own font, not the display
     face inside it, and boxes an 80px headline to the width of 20 body digits. */
  max-width: min(90vw, 46rem);
  /* JS owns this. The first beat stays visible if the script never runs, so the
     landing view always has a headline on it. */
  opacity: 0;
  will-change: opacity, transform;
}
.beat:first-child { opacity: 1; }
.beat--wide { max-width: min(92vw, 60rem); }
.beat--peak { max-width: min(90vw, 42rem); }

/* The reveal: each line rises out of its own mask. Authored per line, not split
   at runtime, so nothing depends on measuring a font that may still be loading. */
.beat__line { display: block; overflow: hidden; padding-bottom: 0.12em; margin-bottom: -0.06em; }
.beat__line > span {
  display: block;
  transform: translate3d(0, var(--e-rise, 0%), 0);
  will-change: transform;
}

.beat__support { margin-top: 1.6rem; }

/* Density only where the type is. No full-frame overlay. */
.film__scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  /* A corner of density sized to the copy block, not a band across the frame.
     An edge gradient has to darken the full width to cover one corner; this
     puts the weight where the type is and leaves the photograph alone. */
  background: radial-gradient(
    118% 106% at 0% 100%,
    color-mix(in srgb, var(--e-scrim) 78%, transparent) 0%,
    color-mix(in srgb, var(--e-scrim) 58%, transparent) 32%,
    color-mix(in srgb, var(--e-scrim) 24%, transparent) 56%,
    transparent 76%
  );
  opacity: var(--e-scrim-a, 1);
}

/* ------------------------------------------------------- flow  sections --- */

.e-section { padding-block: clamp(5rem, 12vh, 9.5rem); position: relative; }
.e-wrap { width: min(100% - 2 * clamp(1rem, 3.4vw, 2.6rem), 1360px); margin-inline: auto; }

/* More space above a heading than below it: the gap belongs to the boundary. */
.e-head { margin-bottom: clamp(2.6rem, 5vh, 4rem); }
.e-head .e-body { margin-top: 1.5rem; }

/* Services: an asymmetric two column split, not a card grid and not three
   equal tiles. The rule between rows is the only chrome. */
.svc { display: grid; gap: clamp(1.5rem, 3vw, 3rem) clamp(2rem, 5vw, 5rem); }
.svc__row {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(1rem, 3vw, 3rem);
  padding-block: clamp(1.9rem, 4vh, 3rem);
  border-top: 1px solid var(--e-hair);
  align-items: start;
}
.svc__row:last-child { border-bottom: 1px solid var(--e-hair); }
.svc__name { margin: 0; }
.svc__detail { margin: 0; }
.svc__list { list-style: none; margin: 1.1rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem 1.4rem; }
.svc__list li { font-size: 0.8125rem; color: var(--sc-ink-soft); letter-spacing: 0.01em; }

.svc__figure { margin: clamp(2.6rem, 6vh, 4.5rem) 0 0; }
.svc__figure img { display: block; width: 100%; height: auto; border-radius: var(--e-radius); }
.svc__cap { margin: 0.9rem 0 0; font-size: 0.8125rem; color: var(--sc-ink-soft); }

/* Showcase rail. Lateral travel reads as breadth. */
.rail {
  display: flex;
  align-items: stretch;
  gap: clamp(1.25rem, 2.4vw, 2.2rem);
  padding-inline: clamp(1rem, 3.4vw, 2.6rem);
  height: 100%;
  will-change: transform;
}
.rail__lead {
  flex: 0 0 clamp(17rem, 26vw, 24rem);
  align-self: center;
  padding-right: clamp(1rem, 2vw, 2rem);
}
.rail__lead .e-body { margin-top: 1.3rem; }

.work {
  flex: 0 0 clamp(15.5rem, 25vw, 22rem);
  align-self: center;
  display: flex;
  flex-direction: column;
  /* Items settle in sequence so the rail reads as a drawer being pulled rather
     than a slideshow. Floor the opacity so an unsettled card reads as arriving,
     not as failing to load. estoiko.js writes --e-settle. */
  opacity: var(--e-settle, 1);
  transform: translate3d(0, calc((1 - var(--e-settle, 1)) * 26px), 0);
}
.work__shot { margin: 0; overflow: hidden; background: var(--sc-surface); }
.work__shot img { display: block; width: 100%; height: auto; aspect-ratio: 4 / 5; object-fit: cover; }
.work__meta { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin-top: 1rem; }
.work__name { font-family: var(--sc-font-display); font-size: 1.35rem; font-weight: 500; letter-spacing: -0.01em; margin: 0; }
.work__year { font-size: 0.75rem; color: var(--sc-ink-soft); letter-spacing: 0.08em; font-variant-numeric: tabular-nums; }
.work__kind { margin: 0.5rem 0 0; font-size: 0.8125rem; color: var(--sc-ink-soft); }
.rail__end {
  flex: 0 0 clamp(13rem, 20vw, 18rem);
  align-self: center;
}

/* ---------------------------------------------------------------- close --- */

.close__stage {
  display: grid;
  place-items: center;
  padding: calc(var(--e-bar-h) + 1rem) clamp(1rem, 3.4vw, 2.6rem) clamp(1.5rem, 4vh, 3rem);
}
.close__inner {
  width: min(100%, 1360px);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(2rem, 6vw, 6rem);
  align-items: start;
}

.form { display: grid; gap: 1.15rem; }
.field { display: grid; gap: 0.45rem; }
.field > label {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sc-ink-soft);
}
.field input, .field textarea {
  font-family: inherit;
  font-size: 0.95rem;
  color: var(--sc-ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--e-hair2);
  border-radius: var(--e-radius);
  padding: 0.6rem 0 0.55rem;
  transition: border-color 240ms var(--e-ease);
}
.field input::placeholder, .field textarea::placeholder { color: color-mix(in srgb, var(--sc-ink) 46%, transparent); }
/* The thickened accent rule is the resting focus treatment. The outline is only
   suppressed for pointer focus: killing it on plain :focus out-specifies the
   :focus-visible rule above and leaves keyboard users with no ring at all. */
.field input:focus, .field textarea:focus {
  border-bottom-color: var(--sc-accent);
  border-bottom-width: 2px;
  padding-bottom: calc(0.55rem - 1px);
}
.field input:focus:not(:focus-visible),
.field textarea:focus:not(:focus-visible) { outline: none; }
.field textarea { resize: vertical; min-height: 5.5rem; }
.field__hint { font-size: 0.75rem; color: var(--sc-ink-soft); }
.field__error { font-size: 0.75rem; color: var(--sc-accent); min-height: 1rem; }
.field--invalid input, .field--invalid textarea { border-bottom-color: var(--sc-accent); }

.form__foot { display: flex; align-items: center; gap: 1.25rem; flex-wrap: wrap; margin-top: 0.4rem; }
.form__status { font-size: 0.8125rem; color: var(--sc-ink-soft); }

.e-foot {
  border-top: 1px solid var(--e-hair);
  padding: 1.5rem 0 0;
  margin-top: clamp(2.5rem, 6vh, 4rem);
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  font-size: 0.8125rem;
  color: var(--sc-ink-soft);
}
.e-foot a { color: inherit; text-decoration: none; border-bottom: 1px solid var(--e-hair2); }
.e-foot a:hover { color: var(--sc-ink); }

/* ------------------------------------------------------------- responsive - */

/* Below 900px the film stops being a backdrop and becomes a plate, with the
   type on the lit ground beneath it. A 16:9 frame cropped to a phone's 9:16
   throws away most of every shot, and copy over a small busy frame is the
   least legible thing on the page. Same story, same sync, different
   composition. */
@media (max-width: 900px) {
  :root { --e-bar-h: 58px; }

  .film__stage {
    display: grid;
    /* The copy row is `auto`, so it takes exactly the height of the tallest
       beat and never more; the plate takes everything that is left. That is
       what keeps the media around 70% of the screen on a normal phone while
       making it impossible for the copy to be pushed off the bottom on a short
       one. A fixed 70svh plate does not survive a 667px viewport. */
    grid-template-rows: minmax(0, 1fr) auto;
    padding-top: calc(var(--e-bar-h) + 0.5rem);
    gap: 0;
  }
  .film__plate {
    position: relative;
    inset: auto;
    /* Full bleed. Previously this had a fixed margin AND both aspect-ratio and
       max-height: once the height cap bound, the box became narrower than the
       column and a non-auto margin left it stranded against the left edge. */
    margin-inline: 0;
    width: 100%;
    height: 100%;
    aspect-ratio: auto;
    max-height: none;
    /* Full bleed here too now, so it dissolves into the ground the same way the
       desktop stage does instead of ending on a hard line above the copy. */
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 90%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 0%, #000 90%, transparent 100%);
  }
  .film__plate > video, .film__plate > .sc-stage__poster { object-position: 50% 40%; }
  .film__scrim { display: none; }

  .film__copy {
    position: relative;
    inset: auto;
    padding: clamp(1rem, 2.4vh, 1.5rem) clamp(1rem, 4vw, 2rem) clamp(1.25rem, 3vh, 2rem);
    align-self: end;
    /* The copy rests on the wash, not on the raw ground. The ground travels
       through mid tones where small text cannot reach 4.5:1 against anything,
       and here there is no photograph to scrim against. It fades in from the
       plate so it never reads as a panel, and because the wash is the ink's own
       opposite it stays invisible except exactly when it is doing work. The
       ground is still on screen framing the plate, so the signature holds. */
    background: linear-gradient(
      to bottom,
      transparent 0,
      color-mix(in srgb, var(--e-scrim) 90%, transparent) clamp(1.1rem, 3vh, 1.8rem),
      color-mix(in srgb, var(--e-scrim) 90%, transparent) 100%
    );
  }
  /* Desktop anchors the beats to the bottom of a full-bleed frame. Here the
     copy has its own space under the plate, so it hangs from the top of it
     instead; inheriting `end` drops every line to the foot of the screen and
     leaves a dead gap under the picture. */
  .beat { max-width: 100%; align-self: start; }
  .e-display--xl { font-size: clamp(2.2rem, 9.2vw, 3.1rem); }
  .e-lede { max-width: 42ch; font-size: 0.95rem; }
  .beat__support { margin-top: 1.15rem; }

  .close__inner { grid-template-columns: minmax(0, 1fr); gap: 2.5rem; }
  .svc__row { grid-template-columns: minmax(0, 1fr); gap: 0.9rem; }
}

@media (max-width: 560px) {
  .e-bar { padding-inline: 1rem; }
  .e-mark { font-size: 0.95rem; letter-spacing: 0.22em; }
  .e-cta { padding: 0.62rem 0.85rem; font-size: 0.625rem; letter-spacing: 0.14em; }
}

/* Touch targets, and no hover-only affordances on a device without a pointer. */
@media (hover: none) {
  .e-cta:hover { background: transparent; color: var(--sc-ink); border-color: var(--e-hair2); }
  .e-cta--solid:hover { background: var(--sc-ink); color: var(--sc-canvas); }
  .e-cta { min-height: 44px; }
}

/* --------------------------------------------------------- reduced motion - */

@media (prefers-reduced-motion: reduce) {
  /* No clip is ever fetched here, so a pinned stage would hold one static
     poster for six viewport-heights of scroll. Let the act become an ordinary
     section instead: the frame, then the four messages as an editorial stack.
     `!important` is needed because the engine sets the pin geometry inline. */
  .film { height: auto !important; }
  .film__stage {
    position: static !important;
    height: auto !important;
    overflow: visible !important;
    display: block;
    padding: calc(var(--e-bar-h) + 1.5rem) 0 clamp(3rem, 8vh, 5rem);
  }
  .film__plate {
    position: relative;
    inset: auto;
    aspect-ratio: 16 / 9;
    max-width: min(100% - 2 * clamp(1rem, 3.4vw, 2.6rem), 1360px);
    margin-inline: auto;
    max-height: none;
    /* Reset what the phone breakpoint sets. Its `height: 100%` refers to a grid
       row that does not exist once the act is unpinned, and the dissolve belongs
       to a full-bleed band, not to this framed one. */
    height: auto;
    width: auto;
    -webkit-mask-image: none;
    mask-image: none;
  }
  .film__scrim { display: none; }
  .film__copy {
    position: static;
    display: block;
    max-height: none;
    padding: clamp(2.5rem, 6vh, 4rem) clamp(1rem, 3.4vw, 2.6rem) 0;
  }
  .beat { position: relative; align-self: auto; max-width: min(92vw, 60rem); }
  .beat__line > span { transform: none !important; }
  .work { opacity: 1 !important; transform: none !important; }
  .film__plate::after { display: none; }
}
