/* ==========================================================================
   Nasi Forge — assets/css/styles.css
   Foundation layer: design tokens, base/reset, keyframes.

   Ported from the Claude Design artifact `.design-source/Nasi Forge.dc.html`
   (project "Nasi Forge Website Design"). Token values and keyframes are lifted
   verbatim from that source — do not invent or "tidy" them.

   Mobile-first: everything outside a media query is the phone composition —
   one column of cards, normal flow, the page scrolls. Two min-width
   breakpoints build up from there; 1100px assembles the 4 x 2 scatter.
   Order: tokens, base/reset, keyframes, components, breakpoints, hover/focus,
   pointer capability, reduced motion — later sections rely on it.
   ========================================================================== */

:root {
  /* Ink */
  --ink: #17150f;

  /* Text tones. Each is the lowest alpha that still clears WCAG AA (4.5:1) against the
     darkest surface it can land on, so no state fails. Worst case for card text is a
     hovered card's lower half, where the heat bloom warms the backdrop to rgb(243,231,220);
     worst case for page text is the gradient's far edge, #d9d4c9. The original design's
     tonal ORDER is kept (title > body > tagline > label = corner) — only the floor moved.
       body    .68 -> 5.77:1      tagline .66 -> 4.96:1
       label   .64 -> 5.07:1      corner  .64 -> 4.67:1                                  */
  --text-body: rgba(23,21,15,0.68);
  --text-tagline: rgba(23,21,15,0.66);
  --text-label: rgba(23,21,15,0.64);
  --text-corner: rgba(23,21,15,0.64);

  /* The link underline is a link's only affordance here, so WCAG 1.4.11 applies to it as
     a non-text UI component: 3:1 minimum. At the old 0.30 it was 1.95:1. 0.50 -> 3.37:1. */
  --rule: rgba(23,21,15,0.50);

  /* Decorative hairline on the cards — not an affordance, so no contrast floor applies. */
  --ink-13: rgba(23,21,15,0.13);

  /* Accents */
  --accent: #6b5f3f;
  --ember: #b4622a;
  --ember-wash: rgba(180,98,42,0.14);

  /* Surfaces */
  --paper: #e4e0d7;
  /* The rice: the off-white band across the bottom of the page, and the surface the
     steam column rises from. Warmer than white, lighter than --paper. */
  --rice: #faf8f1;
  /* Height of the band, and the height the steam column starts from — one value so the
     two cannot drift apart, and ONE formula at every width so the band is the same
     fraction of the viewport everywhere. The 172px floor only binds on viewports under
     860px tall, and exists so the band can always hold its own content: ~162px for the
     flow bar, and 133px + the 4vh offset for the absolutely placed desktop lockup. Keep
     both under 172px, or the band stops matching this token and the steam drifts off the
     rice line. */
  --ground-h: max(20vh, 172px);
  --card: rgba(255,254,251,0.74);
  --card-quiet: rgba(255,254,251,0.70);
  --card-hover: rgba(255,254,251,0.96);
  --card-border: rgba(23,21,15,0.13);
  --card-shadow: 0 16px 34px -22px rgba(23,21,15,0.5);
  --card-shadow-hover: 0 26px 44px -24px rgba(23,21,15,0.55);
  /* Suspended, not lifted: a shadow thrown further down and softer, plus a close
     contact shadow so the card still reads as a physical object. */
  --card-shadow-float:
    0 36px 52px -28px rgba(23,21,15,0.40),
    0 12px 22px -16px rgba(23,21,15,0.22);
  --page-bg: radial-gradient(120% 90% at 50% 100%, #f2efe9 0%, #e6e2d9 45%, #d9d4c9 100%);

  /* Type */
  --font-sans: 'Space Grotesk', Helvetica, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;

  /* Motion */
  --ease: cubic-bezier(0.2, 0.8, 0.2, 1);

  /* Interaction */
  --card-dim: 0.62;

  /* Metrics — the design's 120px from the first breakpoint. */
  --logo-w: 104px;
}

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

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

html, body { margin: 0; padding: 0; min-height: 100%; background: var(--paper); }

a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--rule); }
a:hover { color: var(--accent); border-bottom-color: var(--accent); }

/* Visually hidden, still announced by screen readers. */
.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;
}

/* --------------------------------------------------------------------------
   Keyframes — ported verbatim from the design source
   -------------------------------------------------------------------------- */

@keyframes nf-rise {
  0%   { transform: translateY(18%) scale(0.5); opacity: 0; }
  14%  { opacity: 1; }
  68%  { opacity: 0.5; }
  100% { transform: translateY(-92vh) scale(1.75); opacity: 0; }
}
@keyframes nf-sway {
  from { transform: translateX(-1em); }
  to   { transform: translateX(1em); }
}
@keyframes nf-float {
  from { transform: translate3d(0, -3px, 0) rotate(-0.3deg); }
  to   { transform: translate3d(0, 3px, 0) rotate(0.32deg); }
}
/* Hovered cards leave the idle drift for a slower, deeper bob — caught by the updraft
   rather than held still. Starts near nf-float's range so the swap does not snap. */
@keyframes nf-buoy {
  from { transform: translate3d(0, -2px, 0) rotate(-0.35deg); }
  to   { transform: translate3d(0, -9px, 0) rotate(0.4deg); }
}
/* The halo behind the masthead logo: steam gathering around the mark. Breathes rather
   than pulses, and drifts fractionally upward so it reads as rising. */
@keyframes nf-halo {
  from { transform: translate(-50%, -46%) scale(0.9);  opacity: 0.72; }
  to   { transform: translate(-50%, -52%) scale(1.08); opacity: 1; }
}
@keyframes nf-in {
  from { opacity: 0; transform: translateY(16px) scale(0.985); filter: blur(3px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}

/* Two entrance keyframes for the brand, one per centring scheme. The base is
   nf-logo-in-flow: in flow .brand is centred by `margin: … auto`, so the
   animation may only translate on Y. nf-logo-in is the design source's
   original, carrying the translate(-50%, …) that pairs with the absolute
   `left: 50%` used from 1100px. Swapping the animation swaps the scheme. */

@keyframes nf-logo-in-flow {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}
@keyframes nf-logo-in {
  from { opacity: 0; transform: translate(-50%, 18px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

/* --------------------------------------------------------------------------
   Stage — a viewport tall at least, free to grow past it while the page scrolls
   -------------------------------------------------------------------------- */

.stage {
  position: relative;
  width: 100%;
  min-height: 100vh;
  /* A column so .ground can be pushed to the bottom edge with margin-top: auto. Without
     it the band ends where the content ends and leaves page background below it. */
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--page-bg);
  font-family: var(--font-sans);
  color: var(--ink);
}

/* Paragraph margins are set explicitly per component below. */
p { margin: 0; }

/* --------------------------------------------------------------------------
   Steam — decorative layer; .steam__field is populated by assets/js/steam.js
   -------------------------------------------------------------------------- */

.steam {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
}

.steam__field {
  position: absolute;
  left: 0;
  right: 0;
  /* Anchored to the top of the rice band so the column reads as steam coming off it. */
  bottom: var(--ground-h);
  height: 60vh;
  /* Short bottom fade — the steam has to appear to leave the rice, not materialise
     well above it. The long top fade is what makes it dissipate. */
  mask-image: linear-gradient(to top, transparent 0%, #000 4%, #000 70%, transparent 100%);
  -webkit-mask-image: linear-gradient(to top, transparent 0%, #000 4%, #000 70%, transparent 100%);
}

/* --------------------------------------------------------------------------
   Card grid — one stacked column, two from 720px, the 4 x 2 scatter at 1100px
   -------------------------------------------------------------------------- */

.grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  justify-content: center;
  gap: 22px;
  padding: 44px 20px 0;
}

/* Two wrappers per card: .cell owns the entrance transform, .cell__float the
   idle drift (from 720px — drift fights with scrolling on a phone). Both
   animate `transform`, so they must not share an element. */

.cell {
  align-self: start;
  justify-self: stretch;
  animation: nf-in 1s var(--ease) var(--in-delay) both;
}

/* Per-cell metrics: --title-size / --title-lh / --body-lh feed .card, --w the
   card's width in the desktop scatter — so it is read only from 1100px. */

.cell--1 {
  --w: 250px;
  --title-size: 17px;
  --title-lh: 1.3;
  --body-lh: 1.6;
  --in-delay: 0.35s;
  --float-dur: 11s;
  --float-delay: -1.4s;
}

.cell--2 {
  --w: 226px;
  --title-size: 17px;
  --title-lh: 1.3;
  --in-delay: 0.75s;
  --float-dur: 13s;
  --float-delay: -5s;
}

.cell--3 {
  --w: 222px;
  --title-size: 17px;
  --title-lh: 1.3;
  --in-delay: 1.15s;
  --float-dur: 10s;
  --float-delay: -3.2s;
}

.cell--4 {
  --w: 226px;
  --title-size: 17px;
  --title-lh: 1.3;
  --in-delay: 0.95s;
  --float-dur: 12s;
  --float-delay: -7s;
}

.cell--5 {
  --w: 244px;
  --title-size: 21px;
  --title-lh: 1.15;
  --in-delay: 1.9s;
  --float-dur: 9.5s;
  --float-delay: -6.3s;
}

.cell--6 {
  --w: 232px;
  --title-size: 19px;
  --title-lh: 1.2;
  --in-delay: 2.2s;
  --float-dur: 11.5s;
  --float-delay: -4.4s;
}

.cell--7 {
  --w: 218px;
  --title-size: 19px;
  --title-lh: 1.2;
  --in-delay: 2.5s;
  --float-dur: 12.5s;
  --float-delay: -8.1s;
}

.cell--8 {
  --w: 238px;
  --title-size: 19px;
  --title-lh: 1.2;
  --in-delay: 1.5s;
  --float-dur: 14s;
  --float-delay: -2.1s;
}

/* --------------------------------------------------------------------------
   Card
   -------------------------------------------------------------------------- */

/* `font-size` is the disclosure: .card__body inherits it, so 0 collapses the
   description to zero height (its em-based margin collapses with it) and hover
   animates it back to 11px. Open is the base state; the collapse is bought at
   720px and handed back by the coarse-pointer query. */

.card {
  width: 100%;
  max-width: 100%;
  font-size: 11px;
  padding: 20px 18px;
  border-radius: 3px;
  border: 1px solid var(--card-border);
  background: var(--card);
  backdrop-filter: blur(7px) saturate(1.06);
  -webkit-backdrop-filter: blur(7px) saturate(1.06);
  box-shadow: var(--card-shadow);
}

.card--quiet {
  background: var(--card-quiet);
}

.card__label {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-label);
  margin-bottom: 9px;
}

.card--quiet .card__label { margin-bottom: 8px; }

.card__title {
  font-family: var(--font-sans);
  font-size: var(--title-size);
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: var(--title-lh);
}

.card__body {
  font-family: var(--font-mono);
  font-size: inherit;
  line-height: var(--body-lh, 1.55);
  color: var(--text-body);
  margin-top: 1.4em;
}

/* --------------------------------------------------------------------------
   Brand block — logo + tagline, centred under the cards
   -------------------------------------------------------------------------- */

/* No `transform: translateX(-50%)` here, nor in nf-logo-in-flow: the -50%
   belongs to the absolute desktop position, where nf-logo-in supplies it. */

/* --------------------------------------------------------------------------
   The ground — the rice. An off-white band across the bottom of the page,
   carrying the brand block and the footer.
   -------------------------------------------------------------------------- */

.ground {
  position: relative;
  margin-top: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  background: var(--rice);
  min-height: var(--ground-h);
  padding: 18px 20px;
}

/* Soft lead-in above the band. A hard edge reads as a rectangle laid over the
   page; this lets the rice bloom upward into the steam instead. */
.ground::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 70px;
  background: linear-gradient(to top, var(--rice), rgba(250, 248, 241, 0));
  pointer-events: none;
}

/* A masthead in flow layouts, the bottom lockup on desktop. As a direct child of the
   flex-column .stage it stretches full width, so it centres its own contents. */
.brand {
  margin: 0;
  padding: 30px 20px 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  animation: nf-logo-in-flow 1.4s var(--ease) 0.05s both;
}

.brand__mark {
  position: relative;
  /* Never let flex compress the mark. As a row item it was shrinking to 74px at 390px wide
     and 58px at 320px; the tagline gives up width instead. */
  flex: none;
  /* Own stacking context so the halo's z-index: -1 stays behind the mark and cannot fall
     behind .stage's background — .brand's animation would supply one, but reduced motion
     removes it. */
  isolation: isolate;
  width: calc(var(--logo-w) + 12px);
  aspect-ratio: 275 / 210;
  max-width: 86vw;
}

/* Circular steam behind the mark, lifting it off the flat page. Flow layouts only — on
   desktop the lockup already sits at the foot of the steam column. */
.brand__mark::before {
  content: "";
  position: absolute;
  z-index: -1;
  left: 50%;
  top: 50%;
  width: 225%;
  aspect-ratio: 1;
  transform: translate(-50%, -49%) scale(1);
  background: radial-gradient(circle at 50% 50%,
    rgba(255, 255, 254, 0.95) 0%,
    rgba(255, 255, 253, 0.5) 36%,
    rgba(255, 255, 252, 0) 70%);
  filter: blur(15px);
  pointer-events: none;
  animation: nf-halo 7.5s ease-in-out infinite alternate;
}

.brand__mark img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
}

.brand__tagline {
  /* Defensive: a flex item's automatic minimum size is min-content, so a longer tagline
     could refuse to shrink and overflow the row. Nothing overflows today. */
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.55;
  letter-spacing: 0.05em;
  color: var(--text-tagline);
  max-width: 260px;
  text-wrap: pretty;
}

/* --------------------------------------------------------------------------
   Corner labels — a centred row under the brand until the stage has corners
   -------------------------------------------------------------------------- */

footer {
  display: flex;
  justify-content: center;
  gap: 18px;
}

.corner {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-corner);
}

/* --------------------------------------------------------------------------
   Breakpoint 1 — 720px: two columns, collapsed cards, idle drift
   -------------------------------------------------------------------------- */

@media (min-width: 720px) {

  :root { --logo-w: 120px; }

  /* Capped columns, which the base `justify-content` then centres. Left as
     1fr each, the two columns splay to the edges of a 1024px window and the
     composition breaks into two clusters with a dead void between them. */
  .grid {
    grid-template-columns: repeat(2, minmax(0, 300px));
    gap: 28px 18px;
    padding: 56px 26px 0;
  }

  .cell__float {
    animation: nf-float var(--float-dur) ease-in-out var(--float-delay) infinite alternate;
  }

  .card { font-size: 0; padding: 15px 17px 16px; }
  .card--quiet { padding: 14px 17px 15px; }

  .steam__field { height: 72vh; }

  footer { gap: 28px; }
}

/* --------------------------------------------------------------------------
   Breakpoint 2 — 1100px: the design as drawn. The stage is exactly one
   viewport, the page stops scrolling, and everything is placed against it.
   -------------------------------------------------------------------------- */

@media (min-width: 1100px) {

  html, body { height: 100%; min-height: auto; overflow: hidden; }

  .stage { height: 100vh; min-height: auto; padding-bottom: 0; }

  /* The band is the bottom fifth of the viewport. The floor keeps the brand block
     from outgrowing it on short screens, where 20vh is less than the block is tall
     (it needs ~170px: a 133px brand block clearing its 4vh offset). */
  .ground {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: var(--ground-h);
    min-height: 0;
    display: block;
    padding: 0;
  }

  .ground::before { height: 9vh; }

  /* Starts at the rice line and fills the rest of the viewport. */
  .steam__field { bottom: var(--ground-h); height: calc(100vh - var(--ground-h)); }

  .grid {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: max(240px, calc(96vh - 230px));
    grid-template-columns: repeat(4, minmax(0, 1fr));
    grid-template-rows: repeat(2, minmax(0, 1fr));
    justify-content: normal;
    gap: 42px 18px;
    padding: 22px 26px 0;
  }

  /* The scatter itself. The top row keeps .cell's `align-self: start`. */
  .cell--1 { grid-column: 1; grid-row: 1; justify-self: start; }
  .cell--2 { grid-column: 2; grid-row: 1; justify-self: center; }
  .cell--3 { grid-column: 3; grid-row: 1; justify-self: end; }
  .cell--4 { grid-column: 4; grid-row: 1; justify-self: end; }
  .cell--5 { grid-column: 1; grid-row: 2; align-self: end; justify-self: start; margin-left: 14px; }
  .cell--6 { grid-column: 2; grid-row: 2; align-self: end; justify-self: center; margin-bottom: 12px; }
  .cell--7 { grid-column: 3; grid-row: 2; align-self: end; justify-self: start; }
  .cell--8 { grid-column: 4; grid-row: 2; align-self: end; justify-self: end; }

  .card { max-width: var(--w); }

  /* Back to the centred stack the design draws. The base lays the lockup out as a bar
     so it fits a 20vh band in flow; with the band absolutely positioned here there is
     room for the original arrangement. */
  /* z-index is load-bearing. .brand has to precede .ground in the DOM so it can lead the
     page in flow layouts, but here both are absolutely positioned and .ground paints its
     opaque rice background — without this the lockup is painted over and disappears. */
  .brand {
    position: absolute;
    left: 50%;
    bottom: 4vh;
    z-index: 2;
    margin: 0;
    padding: 0;
    flex-direction: column;
    gap: 18px;
    animation-name: nf-logo-in;
  }

  .brand__mark::before { content: none; }

  .brand__tagline {
    text-align: center;
    max-width: 420px;
  }

  footer {
    display: block;
    justify-content: normal;
    gap: normal;
    margin-top: 0;
  }

  .corner { position: absolute; bottom: 22px; }

  .corner--left  { left: 26px; }
  .corner--right { right: 26px; }
}

/* --------------------------------------------------------------------------
   Hover & focus — the card opens, catches heat, the rest of the grid recedes
   -------------------------------------------------------------------------- */

/* Everything here relies on plain cascade order: these rules sit after the
   base .card rules and after the breakpoints, so no importance flags are
   needed — the design source only forced them to beat its runtime's inline
   styles.

   The transitions are declared outside the hover query so the card settles
   back along the same curve whatever the pointer type. `font-size` leads the
   list because it *is* the disclosure — see the note above .card. */

.card {
  position: relative;
  cursor: default;
  transition:
    font-size 0.45s var(--ease),
    transform 0.4s var(--ease),
    background 0.45s,
    box-shadow 0.45s,
    backdrop-filter 0.45s,
    -webkit-backdrop-filter 0.45s,
    border-color 0.45s ease;
}

/* Card text sits above both pseudo-elements. Without this the heat bloom, as a
   positioned descendant, would paint over the in-flow paragraphs. */
.card > * { position: relative; z-index: 1; }

/* Ember rule — a 2px seam of heat climbing the left edge from below, inset by
   the 3px corner radius so it never pokes past the rounded corners. */
.card::before {
  content: "";
  position: absolute;
  left: 0;
  top: 3px;
  bottom: 3px;
  width: 2px;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(0deg, var(--ember), var(--accent));
  transform: scaleY(0);
  transform-origin: bottom;
  transition: transform 0.45s var(--ease);
}

/* Heat bloom — warmth pooling at the bottom edge, anchored bottom-centre on
   the same axis as --page-bg and the steam. */
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  border-radius: inherit;
  background: radial-gradient(120% 90% at 50% 100%, var(--ember-wash), transparent 70%);
  opacity: 0;
  transition: opacity 0.45s var(--ease);
}

/* The eyebrow sharpens as you approach. */
.card__label {
  transition:
    color 0.45s var(--ease),
    letter-spacing 0.45s var(--ease);
}

/* .cell__float is the one link in the chain where the sibling dim can live.
   .cell runs `nf-in … both`, and an animation's filled `to` frame (opacity: 1)
   beats any normal declaration, so dimming .cell would silently do nothing.
   nf-float only animates `transform`, leaving .cell__float's opacity free to
   be set and transitioned. */
.cell__float {
  position: relative;
  /* Forces a stacking context so the plume's z-index: -1 stays behind the card and does
     not fall behind .stage's background. nf-float's transform would do this too, but not
     under prefers-reduced-motion, where the animation is removed. */
  isolation: isolate;
  transition: opacity 0.35s ease;
}

/* The steam the card floats in. A soft plume behind and below it, blooming upward on
   hover so the card reads as suspended in the column rather than sitting on the page. */
.cell__float::before {
  content: "";
  position: absolute;
  z-index: -1;
  left: 50%;
  bottom: -20%;
  width: 152%;
  height: 168%;
  transform: translateX(-50%) translateY(8px) scale(0.88);
  background: radial-gradient(ellipse at 50% 68%,
    rgba(255, 255, 254, 0.9) 0%,
    rgba(255, 255, 253, 0.42) 42%,
    rgba(255, 255, 252, 0) 72%);
  filter: blur(16px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s var(--ease), transform 0.6s var(--ease);
}

a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

@media (hover: hover) and (pointer: fine) {

  /* :focus-within gives keyboard parity — the two cards holding links
     (sama2.my, hello@nasiforge.com) open on Tab exactly as they do on hover. */

  .card:hover,
  .card:focus-within {
    font-size: 11px;
    transform: translateY(-8px);
    background: var(--card-hover);
    box-shadow: var(--card-shadow-float);
    border-color: var(--ink);
    backdrop-filter: blur(13px) saturate(1.12);
    -webkit-backdrop-filter: blur(13px) saturate(1.12);
  }

  .card:hover::before,
  .card:focus-within::before { transform: scaleY(1); }

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

  .card:hover .card__label,
  .card:focus-within .card__label {
    color: var(--ink);
    letter-spacing: 0.18em;
  }

  /* Caught by the updraft. The idle drift gives way to a slower, deeper bob, and the
     plume behind the card blooms up around it. :has() is baseline; where it is missing
     both selectors are simply dropped and the card keeps drifting — nothing breaks. */
  .cell__float:has(.card:hover),
  .cell__float:has(.card:focus-within) {
    animation: nf-buoy 3.6s ease-in-out infinite alternate;
  }

  .cell__float:has(.card:hover)::before,
  .cell__float:has(.card:focus-within)::before {
    opacity: 1;
    transform: translateX(-50%) translateY(0) scale(1);
  }

  /* Recede the rest. */
  .grid:has(.card:hover) .cell:not(:has(.card:hover)) .cell__float,
  .grid:has(.card:focus-within) .cell:not(:has(.card:focus-within)) .cell__float {
    opacity: var(--card-dim);
  }
}

/* Coarse pointers never hover, so progressive disclosure would hide half the
   site's content for good. Open every description permanently instead. */
@media (hover: none), (pointer: coarse) {
  .card { font-size: 11px; }
}

/* --------------------------------------------------------------------------
   Reduced motion — strip the decorative animation, keep the composition
   -------------------------------------------------------------------------- */

/* The entrance animations run with `both`, so `animation: none` drops
   elements back to their base style; the opacity/transform/filter resets are
   stated anyway, so a future edit can't leave the cards blurred or invisible.

   In flow .brand needs no transform, so dropping its animation is the whole
   job. From 1100px it is not: the centring there comes from nf-logo-in's
   translate(-50%, 0) paired with `left: 50%`, and dropping the animation
   without restoring that translate shoves the logo half a block-width right.

   Keep this block last, or the `animation-name` in the 1100px breakpoint
   re-arms what `animation: none` just took away. */

@media (prefers-reduced-motion: reduce) {
  .cell {
    animation: none;
    opacity: 1;
    transform: none;
    filter: none;
  }

  .cell__float {
    animation: none;
    transform: none;
  }

  .brand {
    animation: none;
    opacity: 1;
  }

  @media (min-width: 1100px) {
    .brand { transform: translate(-50%, 0); }
  }

  /* The hover state is kept — the ember rule and the heat bloom are state
     changes, not ambient motion, and a colour/opacity crossfade carries no
     vestibular risk. What goes is anything that *travels*: the 4px lift, and
     the two transitions that animate layout (transform, font-size). The
     description still unfurls, it just appears rather than growing. */

  .card {
    transition:
      background 0.45s,
      box-shadow 0.45s,
      border-color 0.45s ease;
  }

  .card::before { transition: none; }

  .card:hover,
  .card:focus-within { transform: none; }

  /* .cell__float:has(...) outranks the plain .cell__float reset above, so the buoy has
     to be cancelled explicitly or hovering would start a loop that reduced motion is
     meant to prevent. */
  .cell__float:has(.card:hover),
  .cell__float:has(.card:focus-within) {
    animation: none;
    transform: none;
  }

  /* Steam halo behind the masthead: present, but not breathing. */
  .brand__mark::before {
    animation: none;
    transform: translate(-50%, -49%) scale(1);
    opacity: 0.9;
  }

  /* The plume still appears — it just does not travel. Parked at its final transform so
     only opacity changes. */
  .cell__float::before {
    transform: translateX(-50%) scale(1);
    transition: opacity 0.5s var(--ease);
  }
}
