/* ==========================================================================
   Hamza's Blades — design system
   Tokens and primitives per SPEC section 2. Nothing here is page-specific
   except the clearly marked home sections at the bottom.
   ========================================================================== */

/* --- Tokens -------------------------------------------------------------- */

:root {
  /* Palette. No colours outside this list. Pure white never appears. */
  --forge: #0a0908;
  --ash: #16130f;
  --ash-line: #241e18;
  --brass: #c89f68;
  --parchment: #e4d9c6;
  --smoke: #8b7b64;
  --ember: #d2662c;
  --ember-hot: #f0a555;

  /* Type */
  --display: "Amiri", "Times New Roman", serif;
  --body: "Karla", system-ui, -apple-system, "Segoe UI", sans-serif;

  --t-hero: clamp(2.75rem, 7vw, 5.5rem);
  --t-page: clamp(2rem, 4.5vw, 3.25rem);
  --t-section: 1.75rem;
  --t-piece: 1.25rem;
  --t-body: 1.0625rem;
  --t-small: 0.9375rem;
  --t-eyebrow: 0.6875rem;

  /* Rhythm — 8px base */
  --s-1: 0.5rem;
  --s-2: 1rem;
  --s-3: 1.5rem;
  --s-4: 2rem;
  --s-5: 3rem;
  --s-6: 4rem;
  --s-7: 6rem;

  --shell: 1280px;
  --measure: 68ch;
  --section-y: clamp(5rem, 12vh, 9rem);

  --line: 1px solid var(--ash-line);
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* --- Reset --------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
}

body,
h1,
h2,
h3,
p,
figure,
blockquote,
ul,
ol {
  margin: 0;
}

ul,
ol {
  padding: 0;
  list-style: none;
}

img,
video,
canvas,
svg {
  display: block;
  max-width: 100%;
}

button,
input {
  font: inherit;
  color: inherit;
}

/* The hidden attribute must beat any display we set on a component. Without
   this, `el.hidden = true` on anything given `display: grid` does nothing —
   which silently leaves the lightbox covering the page and swallowing every
   click. */
[hidden] {
  display: none !important;
}

/* --- Base ---------------------------------------------------------------- */

body {
  background: var(--forge);
  color: var(--parchment);
  font-family: var(--body);
  font-size: var(--t-body);
  font-weight: 400;
  line-height: 1.75;
  /* Establishes the ground the fixed ember canvas screens against. */
  min-height: 100vh;
}

h1,
h2,
h3,
.display {
  font-family: var(--display);
  font-weight: 400;
  line-height: 1.15;
  /* Amiri at 400 with space around it. Never bold display type. */
}

p + p {
  margin-top: var(--s-3);
}

a {
  color: inherit;
  text-decoration: none;
}

:focus-visible {
  outline: 1px solid var(--brass);
  outline-offset: 2px;
}

::selection {
  background: var(--ember);
  color: var(--forge);
}

/* --- Layout primitives --------------------------------------------------- */

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 5vw, 3rem);
}

.section {
  position: relative;
  /* Above the ember canvas. The sections themselves stay transparent, so the
     embers pass behind the type and cards rather than over them. */
  z-index: 2;
  padding-block: var(--section-y);
}

.section--rule {
  border-top: var(--line);
}

.measure {
  max-width: var(--measure);
}

/* --- Type primitives ----------------------------------------------------- */

/* The letterspaced utility treatment. Used for eyebrows, filter labels and
   metadata — the site's connective tissue. Nowhere else. */
.eyebrow {
  font-family: var(--body);
  font-size: var(--t-eyebrow);
  font-weight: 400;
  line-height: 1.4;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--smoke);
}

.eyebrow--brass {
  color: var(--brass);
}

.t-hero {
  font-size: var(--t-hero);
  letter-spacing: 0.02em;
}

.t-page {
  font-size: var(--t-page);
}

.t-section {
  font-size: var(--t-section);
  color: var(--brass);
}

.t-piece {
  font-size: var(--t-piece);
}

.small {
  font-size: var(--t-small);
}

.muted {
  color: var(--smoke);
}

/* --- Buttons ------------------------------------------------------------- */
/* Square. No radius, no shadow. Depth comes from the ember glow only. */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 0 var(--s-4);
  border: 1px solid transparent;
  border-radius: 0;
  background: transparent;
  font-size: var(--t-small);
  letter-spacing: 0.04em;
  cursor: pointer;
  transition: background-color 0.25s var(--ease), border-color 0.25s var(--ease),
    color 0.25s var(--ease);
}

.btn--primary {
  background: var(--ember);
  color: var(--forge);
}

.btn--primary:hover {
  background: var(--ember-hot);
}

.btn--ghost {
  border-color: var(--ash-line);
  color: var(--parchment);
}

.btn--ghost:hover {
  border-color: var(--brass);
  color: var(--brass);
}

/* Eyebrow-styled inline link with an arrow — used for every "through" link. */
.link-through {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  padding-block: var(--s-1);
  color: var(--brass);
  transition: color 0.25s var(--ease);
}

.link-through .arrow {
  transition: transform 0.25s var(--ease);
}

.link-through:hover {
  color: var(--ember-hot);
}

.link-through:hover .arrow {
  transform: translateX(4px);
}

/* --- Header -------------------------------------------------------------- */

.header {
  position: fixed;
  inset: 0 0 auto;
  z-index: 5;
  transition: background-color 0.35s var(--ease), border-color 0.35s var(--ease);
  border-bottom: 1px solid transparent;
}

.header.is-stuck {
  background: rgba(10, 9, 8, 0.94);
  backdrop-filter: blur(8px);
  border-bottom-color: var(--ash-line);
}

.header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  min-height: 72px;
}

/* The brand is type only. The logo mark is not used below ~48px — see the
   note in index.html and SPEC section 7. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
}

/* Capped at 32px and never upscaled, like the hero mark. The wordmark beside
   it carries the name, so the mark does not have to be read — which is what
   makes it usable below SPEC section 7's ~48px legibility floor. */
.brand__mark {
  width: 32px;
  height: 32px;
  flex: none;
}

@media (max-width: 420px) {
  .brand__mark {
    width: 26px;
    height: 26px;
  }
}

.brand__name {
  font-family: var(--display);
  font-size: 1.0625rem;
  letter-spacing: 0.04em;
  color: var(--parchment);
  white-space: nowrap;
}

.nav {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2.5vw, 2rem);
}

.nav__link {
  font-size: var(--t-small);
  color: var(--parchment);
  padding-block: var(--s-1);
  border-bottom: 1px solid transparent;
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease);
}

.nav__link:hover,
.nav__link[aria-current="page"] {
  color: var(--brass);
  border-bottom-color: var(--brass);
}

/* Narrow: the nav wraps to its own row beneath the wordmark. Every page stays
   reachable — nothing is hidden behind a menu button, and nothing is dropped. */
@media (max-width: 720px) {
  .header__inner {
    flex-direction: column;
    justify-content: center;
    gap: 0;
    padding-block: var(--s-1);
  }
  .nav {
    gap: clamp(0.875rem, 4vw, 1.5rem);
  }
  .nav__link {
    font-size: 0.875rem;
  }
}

/* --- Ember canvas -------------------------------------------------------- */
/* Injected by js/embers.js. The layout is finished without it.

   Stacking, which is the whole seam: the hero sits at z-index 0, the canvas at
   1, everything after the hero at 2. So the simulated sparks are drawn *over*
   the hero video — meeting its real sparks inside the bottom gradient — and
   *behind* the content further down, where they read as atmosphere in the
   gutters instead of litter across the photographs. */

.embers {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  mix-blend-mode: screen;
}

/* --- Hero ---------------------------------------------------------------- */

.hero {
  position: relative;
  z-index: 0;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  align-items: stretch;
  overflow: hidden;
}

.hero__media {
  position: absolute;
  inset: 0;
  z-index: 0;
  /* Stands in for the poster frame until the video file lands, and stays
     behind it afterwards so there is never a bare black rectangle. */
  background: radial-gradient(
      120% 90% at 50% 18%,
      #2a1c11 0%,
      #150f0a 45%,
      var(--forge) 100%
    );
}

.hero__img,
.hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The video is invisible until it can actually play, then fades up over the
   still. Until the file is shot it never becomes ready, so the still is what
   the hero shows. */
.hero__video {
  opacity: 0;
  transition: opacity 0.8s var(--ease);
}

.hero__video.is-ready {
  opacity: 1;
}

/* The seam: bottom 25% dissolves to --forge, and the ember canvas (z-index 2)
   sits over it. Top scrim keeps the header and hero type legible. */
.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    /* The forge below the frame is drawn by the canvas, not here — a glow in
       this box would be clipped at the hero's bottom edge and leave a hard
       line across the page on scroll. See js/embers.js. */
    /* A pool behind the centred type. The hot steel in the still sits at
       roughly half height, right where the title and eyebrow land, and the
       eyebrow in --smoke has no chance against it. Falls off to nothing at
       the edges so the glow survives. */
    radial-gradient(
      52% 28% at 50% 74%,
      rgba(10, 9, 8, 0.5) 0%,
      rgba(10, 9, 8, 0.28) 60%,
      rgba(10, 9, 8, 0) 100%
    ),
    /* Top: keeps the header legible. */
    linear-gradient(
      to bottom,
      rgba(10, 9, 8, 0.72) 0%,
      rgba(10, 9, 8, 0.25) 28%,
      rgba(10, 9, 8, 0) 50%
    ),
    /* Bottom quarter: the dissolve to --forge that the embers rise into. */
    linear-gradient(
      to bottom,
      rgba(10, 9, 8, 0) 60%,
      rgba(10, 9, 8, 0.6) 82%,
      var(--forge) 100%
    );
}

/* Three rows: the mark up top, empty middle where the fire is, the type low.
   The still's hot steel runs across the centre of the frame and nothing
   readable survives on top of it — the eyebrow in --smoke least of all. */
.hero__inner {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-rows: auto 1fr;
  justify-items: center;
  text-align: center;
  /* Top clears the fixed header. */
  padding-block: calc(72px + var(--s-4)) clamp(3rem, 10vh, 6rem);
}

.hero__type {
  /* Fills the remaining row and pins itself to the bottom of it. */
  align-self: end;
  display: grid;
  justify-items: center;
  gap: var(--s-3);
}

/* 120px is the ceiling for the raster logo — never upscaled. */
.hero__mark {
  width: clamp(84px, 12vw, 120px);
  height: auto;
}

.hero__title {
  margin-top: var(--s-1);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-2);
  margin-top: var(--s-3);
}

/* --- Positioning statement ----------------------------------------------- */

.statement {
  text-align: center;
  display: grid;
  justify-items: center;
  gap: var(--s-3);
}

.statement__body {
  font-family: var(--display);
  font-size: clamp(1.25rem, 2.4vw, 1.75rem);
  line-height: 1.6;
  color: var(--parchment);
  max-width: 46ch;
}

/* --- Section headers ----------------------------------------------------- */

.section__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
  margin-bottom: var(--s-5);
}

/* --- Piece grid ---------------------------------------------------------- */

/* Gallery grid. */
.pieces {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: var(--s-4);
}

/* The catalogue arrives after a fetch, so the grid starts as one line of
   fallback text and then becomes several thousand pixels of cards. Without a
   reservation that shoves the whole page down as it lands — a 0.37 layout
   shift, enough on its own to put the page under the spec's Lighthouse floor.
   The class is added once the cards are in, or once the fetch has failed. */
.pieces[data-gallery]:not(.is-loaded) {
  min-height: 80vh;
}

.pieces--featured:not(.is-loaded) {
  min-height: 60vh;
}

/* Featured work is a fixed four, not an auto-fill — the homepage row should
   hold its shape rather than reflowing to three and orphaning the fourth. */
.pieces--featured {
  grid-template-columns: 1fr;
}

@media (min-width: 620px) {
  .pieces--featured {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1100px) {
  .pieces--featured {
    grid-template-columns: repeat(4, 1fr);
  }
}

.piece {
  position: relative;
  /* Its own stacking context, so the glow below can sit at z-index -1 and
     land behind the card's content but still in front of its background.
     Without this the glow escapes into .page's context and disappears
     behind the card entirely. */
  isolation: isolate;
  display: grid;
  gap: var(--s-2);
  padding: var(--s-2);
  border: var(--line);
  background: var(--ash);
  transition: border-color 0.3s var(--ease);
}

.piece::after {
  /* The faint ember glow that rises behind the image on hover. Behind the
     content rather than over it — it used to paint on top, which is why
     .piece__meta carried a z-index to climb back out. That z-index needed
     position: relative, which made .piece__meta the containing block for
     the stretched hit area below, shrinking the clickable area from the
     whole card to the title row. */
  content: "";
  position: absolute;
  z-index: -1;
  inset: auto 0 0;
  height: 60%;
  background: linear-gradient(
    to top,
    rgba(210, 102, 44, 0.22),
    rgba(210, 102, 44, 0)
  );
  opacity: 0;
  transition: opacity 0.4s var(--ease);
  pointer-events: none;
}

.piece:hover,
.piece:focus-within {
  border-color: var(--brass);
}

.piece:hover::after,
.piece:focus-within::after {
  opacity: 1;
}

.piece__frame {
  position: relative;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: linear-gradient(160deg, #1d1811 0%, #100c08 100%);
}

/* Nothing is ever cropped. The frame keeps its 4:5 so the grid rows stay
   even, and the photograph is fitted whole inside it — portrait pieces fill
   it almost exactly, landscape ones sit letterboxed on --ash. Losing half a
   sword to a centre crop is worse than a little empty frame. */
.piece__img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  transition: transform 0.6s var(--ease);
}

/* No zoom: on a contained image it would push the piece past the frame. The
   border warming to --brass and the ember glow carry the hover instead. */

/* Shown when the photograph has not been dropped in yet. Keeps the grid
   at full height so nothing shifts when the real file arrives. */
.piece__pending {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  text-align: center;
}

/* Title over metadata, always stacked — side by side collapses unevenly once
   four cards share the row. */
/* Deliberately not positioned. The hit area in .piece__open::after is
   absolute, so it resolves against the nearest positioned ancestor — make
   this one positioned and the whole card stops being clickable. */
.piece__meta {
  display: grid;
  gap: 2px;
  padding-bottom: var(--s-1);
}

.piece__name {
  color: var(--parchment);
}

.piece--sold {
  opacity: 0.7;
}

/* --- Interior pages ------------------------------------------------------ */
/* Top padding clears the fixed header, which is opaque on these pages. */

.page {
  position: relative;
  z-index: 2;
  padding-block: calc(72px + var(--section-y)) var(--section-y);
}

.page__head {
  display: grid;
  gap: var(--s-2);
  margin-bottom: var(--s-5);
}

/* --- Filters ------------------------------------------------------------- */
/* The eyebrow treatment, as SPEC section 2 asks for filter labels. Active
   gets --brass and a 1px underline. */

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1) var(--s-4);
  padding-bottom: var(--s-3);
  margin-bottom: var(--s-5);
  border-bottom: var(--line);
}

.filter {
  padding: var(--s-1) 0;
  border: 0;
  border-bottom: 1px solid transparent;
  border-radius: 0;
  background: none;
  cursor: pointer;

  font-family: var(--body);
  font-size: var(--t-eyebrow);
  line-height: 1.4;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--smoke);
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease);
}

.filter:hover {
  color: var(--parchment);
}

.filter.is-active {
  color: var(--brass);
  border-bottom-color: var(--brass);
}

.gallery__empty {
  padding-block: var(--s-5);
}

/* --- Piece interaction --------------------------------------------------- */
/* The title is the control; its hit area is stretched over the whole card, so
   the accessible name stays the piece name rather than "image". */

.piece__open {
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

/* Stretches the title's hit area over the whole card, so the photograph is
   clickable and the accessible name is still the piece's name rather than
   "image". This resolves against .piece — see the note on .piece__meta. */
.piece__open::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
}

.piece__open:focus-visible {
  outline: none;
}

.piece:focus-within {
  outline: 1px solid var(--brass);
  outline-offset: 2px;
}

/* Filtering moves the grid rather than snapping it — see the FLIP pass in
   js/gallery.js. */
.piece {
  transition: border-color 0.3s var(--ease), transform 0.4s var(--ease),
    opacity 0.3s var(--ease);
}

.piece.is-hidden {
  display: none;
}

/* --- Retreats ------------------------------------------------------------ */

/* A hero with no logo mark: one row, type pinned low, same dissolve. */
/* The home page pins its hero type low, because the fire runs across the
   middle of the footage and the type has to stay out of it. This page has
   no footage yet — just the gradient — so pinning low left most of a
   viewport of empty space above the title. Centre it instead, and give the
   section rather less than a full screen. */
.hero--page {
  /* Sized by its own content, not by the viewport. A fixed height here left
     a third of a screen of empty gradient under the button before the copy
     below could start, because there is no footage to fill it. The gradient
     and the embers are unaffected — they paint whatever height this is. */
  min-height: 0;
}

.hero--page .hero__inner {
  grid-template-rows: auto;
  padding-block: calc(72px + var(--s-5)) var(--s-6);
}

.hero--page .hero__type {
  align-self: center;
}

/* The hero's waitlist link scrolls rather than jumps. Reduced motion forces
   this back to `auto` in the block at the end of this file. */
html {
  scroll-behavior: smooth;
}

/* Clears the fixed header, so the band's eyebrow is not hidden under it when
   the link lands. */
#waitlist {
  scroll-margin-top: calc(72px + var(--s-3));
}

/* The band takes focus when the link moves to it, so a keyboard carries on
   from there rather than from the top of the page. It is a scroll target,
   not a control, so it must not grow a focus ring. */
#waitlist:focus {
  outline: none;
}

.retreat-blocks {
  display: grid;
  gap: var(--s-6);
}

@media (min-width: 860px) {
  .retreat-blocks {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--s-5);
  }
}

.retreat-block {
  display: grid;
  gap: var(--s-2);
  align-content: start;
  padding-top: var(--s-3);
  border-top: 1px solid var(--brass);
}

/* The email capture. Centred, with room around it. */
.notify-band {
  display: grid;
  justify-items: center;
  gap: var(--s-2);
  text-align: center;
}

.notify-band .measure {
  margin-bottom: var(--s-4);
}

.notify-mount {
  width: 100%;
  display: grid;
  justify-items: center;
}

.notify {
  width: 100%;
  max-width: 32rem;
  gap: var(--s-2);
  text-align: left;
}

/* Two fields, side by side once there is room for both to stay legible.
   Below that they stack, because a name and an address squeezed into half a
   phone each is worse than a second line. */
.notify__fields {
  display: grid;
  gap: var(--s-2);
}

@media (min-width: 560px) {
  .notify__fields {
    grid-template-columns: 1fr 1fr;
  }
}

/* The button sits under the fields rather than beside them. With one field
   it could ride alongside; with two there is no width left for it, and a
   waitlist is worth its own full-width press. */
.notify__submit {
  width: 100%;
  margin-top: var(--s-2);
  min-height: 48px;
}

@media (min-width: 560px) {
  .notify__submit {
    width: auto;
    justify-self: start;
  }
}

.notify__field {
  min-width: 0;
}

/* --- Process steps ------------------------------------------------------- */
/* An ordered list, because the order is the point. Images alternate sides. */

.steps {
  display: grid;
  gap: var(--s-7);
  counter-reset: step;
}

.step {
  display: grid;
  gap: var(--s-3);
  align-items: center;
}

@media (min-width: 860px) {
  .step {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-6);
  }

  /* Alternating side. The copy leads on every second step. */
  .step:nth-child(even) .step__figure {
    order: 2;
  }
}

.step__figure {
  position: relative;
  /* Held until the stage's real ratio is known, so the page reserves space
     and never reflows as footage loads. */
  aspect-ratio: 4 / 3;
  --step-cap: min(68vh, 600px);
  border: var(--line);
  background: linear-gradient(160deg, #1d1811 0%, #100c08 100%);
  overflow: hidden;
}

/* Each stage is shaped to its own footage rather than to a house crop. Five
   of the six were shot vertically on a phone and one landscape, and forcing
   those into a single 4:3 frame would cut roughly two thirds off the tall
   ones. So the frame takes the clip's aspect ratio, and nothing is cropped.

   A 9:16 clip at full column width would be over a thousand pixels tall, so
   height is capped and the width follows from the ratio — a tall stage sits
   narrow and centred in its column, a wide one fills it. --ar is set per
   stage by js/loops.js from data/process.json. */
.step__figure--sized {
  aspect-ratio: var(--ar);
  width: min(100%, calc(var(--step-cap) * var(--ar)));
  margin-inline: auto;
}

/* The still and the loop stack in the same frame. Both start invisible and
   fade up only once they have actually loaded, so a stage that has not been
   shot shows the pending block rather than a broken image or a black box —
   and a stage that has both uses the still as the loop's poster. */
.step__img,
.step__loop {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* contain, not cover: the frame already matches the footage, so this only
     guarantees that a rounding error letterboxes by a hairline rather than
     shaving the edge off the blade. */
  object-fit: contain;
  opacity: 0;
  transition: opacity 0.6s var(--ease);
}

.step__img.is-ready,
.step__loop.is-ready {
  opacity: 1;
}

/* The loop sits over the still, so the handoff is a fade rather than a cut. */
.step__loop {
  z-index: 1;
}

/* Cleared by js/loops.js once media lands. The pending block is also first
   in the markup, so a still that is already there simply paints over it —
   no selector support required, and nothing flashes before script runs. */
.step__figure.is-ready .band__pending {
  display: none;
}

@media (prefers-reduced-motion: reduce) {
  .step__img,
  .step__loop {
    transition: none;
  }
}

.step__copy {
  display: grid;
  gap: var(--s-2);
  align-content: center;
}

/* --- About --------------------------------------------------------------- */

.about {
  display: grid;
  gap: var(--s-5);
  align-items: start;
}

@media (min-width: 900px) {
  .about {
    grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
    gap: var(--s-7);
  }

  .about__media {
    position: sticky;
    top: calc(72px + var(--s-4));
  }
}

.about__media {
  display: grid;
  gap: var(--s-4);
}

.about__figure {
  position: relative;
  aspect-ratio: 4 / 5;
  border: var(--line);
  background: linear-gradient(160deg, #1d1811 0%, #100c08 100%);
  overflow: hidden;
}

/* Same rule as the Process stages and the retreats band: the frame takes the
   photograph's own shape so nothing is cropped. The portrait is 9:16, and the
   4:5 crop this used to do took a third of its height. Capped so a tall
   photograph does not run away down the column. */
.about__figure--sized {
  aspect-ratio: var(--ar);
  --about-cap: min(66vh, 580px);
  width: min(100%, calc(var(--about-cap) * var(--ar)));
}

.about__figure img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.about__meta {
  display: grid;
  gap: var(--s-3);
}

.about__copy p + p {
  margin-top: var(--s-3);
}

/* --- Closing band -------------------------------------------------------- */
/* Points at the inquiry box, per SPEC section 8. */

.closer {
  display: grid;
  justify-items: start;
  gap: var(--s-3);
  margin-top: var(--s-7);
  padding-top: var(--s-5);
  border-top: var(--line);
}

.closer .t-section {
  max-width: 30ch;
  color: var(--brass);
}

/* --- Contact ------------------------------------------------------------- */
/* Minimal, as SPEC section 4 asks: the inquiry box, Instagram, a shipping
   note. No street address, no phone. */

.contact {
  display: grid;
  gap: var(--s-6);
}

@media (min-width: 860px) {
  .contact {
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr);
    gap: var(--s-7);
  }
}

.contact__aside {
  display: grid;
  gap: var(--s-4);
  align-content: start;
}

.contact__block {
  display: grid;
  gap: var(--s-1);
}

.contact__block p + p {
  margin-top: 0;
}

.contact__block a {
  color: var(--brass);
}

/* --- Inquiry boxes ------------------------------------------------------- */
/* SPEC section 5. A name, an email, a message. Square fields, hairline
   borders, --ash ground. Nothing structured. */

.inquiry {
  display: grid;
  gap: var(--s-3);
  max-width: 34rem;
}

.field {
  display: grid;
  gap: var(--s-1);
}

.field__input {
  width: 100%;
  padding: 0.75rem;
  border: var(--line);
  border-radius: 0;
  background: var(--ash);
  color: var(--parchment);
  font-family: var(--body);
  font-size: var(--t-small);
  line-height: 1.6;
  transition: border-color 0.25s var(--ease);
}

.field__input::placeholder {
  color: var(--smoke);
  opacity: 1;
}

.field__input:hover {
  border-color: var(--smoke);
}

.field__input:focus-visible {
  border-color: var(--brass);
}

.field__input--message {
  resize: vertical;
  min-height: 9rem;
}

.field__input[aria-invalid="true"] {
  border-color: var(--ember);
}

/* Errors read in --ember at weight rather than introducing a red. */
.field__error {
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--ember);
}

/* The small text button that writes the suggested message in. */
.inquiry__suggest {
  justify-self: start;
  padding: var(--s-1) 0;
  border: 0;
  border-bottom: 1px solid var(--ash-line);
  border-radius: 0;
  background: none;
  cursor: pointer;

  font-family: var(--body);
  font-size: var(--t-eyebrow);
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--smoke);
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease);
}

.inquiry__suggest:hover {
  color: var(--brass);
  border-bottom-color: var(--brass);
}

.inquiry__submit {
  justify-self: start;
}

.inquiry__status {
  font-size: var(--t-small);
  line-height: 1.6;
}

.inquiry__status.is-error {
  color: var(--ember);
}

.inquiry__done {
  max-width: var(--measure);
  color: var(--brass);
  font-size: var(--t-body);
}

.inquiry__done:focus-visible {
  outline: 1px solid var(--brass);
  outline-offset: 4px;
}

/* --- Lightbox ------------------------------------------------------------ */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 10;
  display: grid;
  place-items: center;
  padding: var(--s-3);
  background: rgba(10, 9, 8, 0.92);
  opacity: 0;
  transition: opacity 0.2s var(--ease);
  overflow-y: auto;
}

.lightbox.is-open {
  opacity: 1;
}

.lightbox__panel {
  position: relative;
  width: min(1080px, 100%);
  max-height: 100%;
  display: grid;
  gap: var(--s-4);
  /* Top padding clears the close button, which would otherwise sit on the
     photograph when the panel stacks. */
  padding: calc(var(--s-2) + 44px) var(--s-4) var(--s-4);
  border: var(--line);
  background: var(--ash);
}

@media (min-width: 860px) {
  .lightbox__panel {
    grid-template-columns: 1.2fr 1fr;
    gap: var(--s-5);
    padding: var(--s-5);
  }
}

.lightbox__close {
  position: absolute;
  top: var(--s-2);
  right: var(--s-2);
  z-index: 3;
  min-height: 44px;
  padding: 0 var(--s-2);
  border: 1px solid transparent;
  border-radius: 0;
  background: none;
  cursor: pointer;
  transition: border-color 0.25s var(--ease);
}

.lightbox__close:hover {
  border-color: var(--brass);
}

.lightbox__close:hover .eyebrow {
  color: var(--brass);
}

.lightbox__media {
  min-width: 0;
}

.piece__frame--large {
  /* A fixed box rather than a fixed ratio: every view of a piece occupies the
     same space whatever its orientation, so the panel does not jump when you
     step through them. */
  height: clamp(300px, 58vh, 620px);
  aspect-ratio: auto;
}

/* The detail photograph is swipeable on a phone, so claim horizontal
   gestures and leave vertical ones to the dialog's own scrolling. */
.lightbox .piece__frame--large {
  touch-action: pan-y;
}

/* Extra views of a piece, under the detail photo. */
.views {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1);
  margin-top: var(--s-2);
}

.views__item {
  width: 68px;
  aspect-ratio: 4 / 5;
  padding: 0;
  border: var(--line);
  border-radius: 0;
  background: var(--ash);
  cursor: pointer;
  overflow: hidden;
  opacity: 0.55;
  transition: opacity 0.25s var(--ease), border-color 0.25s var(--ease);
}

.views__item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.views__item:hover {
  opacity: 0.85;
}

.views__item.is-active {
  opacity: 1;
  border-color: var(--brass);
}

.lightbox__body {
  display: grid;
  gap: var(--s-2);
  align-content: start;
  padding-top: var(--s-4);
}

@media (min-width: 860px) {
  /* Beside a tall photo, the short column reads better centred than stacked
     at the top. */
  .lightbox__body {
    align-content: center;
    padding-top: 0;
  }
}

.lightbox__inquiry {
  display: grid;
  gap: var(--s-3);
  margin-top: var(--s-3);
  padding-top: var(--s-3);
  border-top: var(--line);
}

/* Holds the page still behind the lightbox. */
body.is-locked {
  overflow: hidden;
}

/* --- Teaser bands -------------------------------------------------------- */

.band {
  display: grid;
  gap: var(--s-4);
  align-items: center;
}

@media (min-width: 900px) {
  .band {
    grid-template-columns: 1.35fr 1fr;
    gap: var(--s-6);
  }
}

.band__figure {
  position: relative;
  /* The default for a band whose media has not been shot. A band that knows
     its media's shape overrides this with --ar below. */
  aspect-ratio: 16 / 9;
  --band-cap: min(72vh, 620px);
  border: var(--line);
  background: linear-gradient(160deg, #1d1811 0%, #100c08 100%);
  overflow: hidden;
}

/* Same rule as the Process stages: the frame takes the footage's own shape
   rather than a house 16:9, so nothing is cropped. The retreats film was
   shot vertically, and a centre crop to 16:9 would take two thirds of it.
   Height is capped and the width follows from the ratio. */
.band__figure--sized {
  aspect-ratio: var(--ar);
  width: min(100%, calc(var(--band-cap) * var(--ar)));
  margin-inline: auto;
  /* No frame once there is something in it. The border reads as a frame
     around an empty band, but around edge-to-edge footage it is just a
     line drawn on the picture. It also caused the thin letterbox above and
     below: with border-box sizing the border eats 2px out of the content
     box, so a contained video no longer matched the frame it was given. */
  border: 0;
}

.band__figure img,
.band__loop {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* The loop sits over its poster and fades up once it can play, so the band
   is never an empty box while the film downloads. */
.band__loop {
  z-index: 1;
  opacity: 0;
  transition: opacity 0.6s var(--ease);
}

.band__loop.is-ready {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .band__loop {
    transition: none;
  }
}

/* A portrait film next to the copy wants the columns the other way round:
   the tall frame is narrow, so the copy takes the larger share.

   The column must have a definite width. An `auto` column here collapses
   the figure to nothing, because the figure's own width is a percentage of
   the column and the column is sized from the figure — circular, and it
   resolves to zero. */
@media (min-width: 900px) {
  .band--portrait {
    grid-template-columns: minmax(0, 0.75fr) minmax(0, 1fr);
  }
}

.band__pending {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  text-align: center;
}

.band__copy {
  display: grid;
  gap: var(--s-2);
  align-content: start;
}

/* --- Footer -------------------------------------------------------------- */

.footer {
  position: relative;
  z-index: 2;
  border-top: var(--line);
  padding-block: var(--s-6) var(--s-5);
}

.footer__inner {
  display: grid;
  gap: var(--s-5);
}

@media (min-width: 760px) {
  .footer__inner {
    grid-template-columns: 1.2fr 1fr 1fr;
  }
}

.footer__col {
  display: grid;
  gap: var(--s-2);
  align-content: start;
}

/* The column's own gap sets the rhythm here — the paragraph rule would
   double it. */
.footer__col p + p {
  margin-top: 0;
}

.footer__links {
  display: grid;
  gap: var(--s-1);
}

.footer__links a {
  font-size: var(--t-small);
  color: var(--parchment);
  transition: color 0.25s var(--ease);
}

.footer__links a:hover {
  color: var(--brass);
}

.footer__base {
  margin-top: var(--s-5);
  padding-top: var(--s-3);
  border-top: var(--line);
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--s-2);
}

/* --- Motion preferences -------------------------------------------------- */
/* Honoured throughout, not just for particles. */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }


  .link-through:hover .arrow {
    transform: none;
  }
}
