/* ECO POWERTECH — sections
   Per-section layout. Same rules as base.css: no colour, size, duration or
   easing literal; semantic tokens only.

   Three pinning mechanics come from research/03-references.md, and all three
   are plain `position: sticky` — no library, nothing animated, zero cost
   against the transform/opacity budget, and each disables to a correct static
   layout by dropping one declaration:
     · hero        two-layer co-pin + release-on-contact  (Terminal, CURA)
     · .rail       sticky spec-rail                        (Kraken)
     · .proc__step stacked sticky steps                    (CRYOX)
   ------------------------------------------------------------------------- */


/* ══ NAV ════════════════════════════════════════════════════════════════ */

.nav {
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  gap: var(--s-8);
  /* Locked height. The hero is pulled up by exactly --nav-h so its 100svh stage
     starts at the top of the viewport; if the nav could drift taller than the
     token that arithmetic silently breaks and the dead zone comes back. */
  min-height: var(--nav-h);
  padding: var(--s-4) var(--gutter);
  border-bottom: var(--hairline-w) solid var(--hairline);
  background: var(--bg);
}

/* inline-flex + --tap-min: the logo is the "home" affordance and the SVG's own
   aspect ratio left it a 38px-tall target on a phone. */
.nav__logo { flex: none; display: inline-flex; align-items: center; min-height: var(--tap-min); }
/* Phase 9d: 2 -> 1.3. The lockup rendered 192x47 next to 11px nav links, so its
   wordmark read at roughly 19px - the logo was nearly twice the size of every
   other word in the bar and pulled the eye off the navigation. At 1.3 it is
   125x30 and the wordmark lands near the link size, which is what "match the
   text" means for a lockup: the WORDMARK matches, and the mark keeps its own
   proportion inside the artwork. */
.nav__logo-svg {
  width: calc(var(--s-24) * 1.3);
  max-width: 40vw;
  height: auto;
  color: var(--fg);
}

.nav__links {
  display: none;
  gap: var(--s-6);
  margin-inline-start: auto;
  font-family: var(--font-mono);
  font-size: var(--eyebrow);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}
.nav__links a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding-block: var(--s-2);
  border-bottom: var(--hairline-w) solid transparent;
  transition: color var(--dur-hover) var(--ease-out),
              border-color var(--dur-hover) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .nav__links a:hover { color: var(--fg); border-bottom-color: var(--accent); }
}

.nav__actions {
  display: flex;
  gap: var(--s-2);
  margin-inline-start: auto;
}
.nav__actions > a.btn--ghost { display: none; }

/* ---- Compact navigation, 320-899px --------------------------------------
   A <details> disclosure. No JavaScript, no focus trap, no scroll lock — the
   panel is 5 links, it is not a modal, and treating it as one is how these
   end up broken with JS disabled. */

.nav__menu { position: static; }          /* the PANEL positions against .nav */

/* summary carries .btn, so it inherits the whole button system — sizing, mono
   caps, focus ring, scale(0.97) on press. Only the marker has to go. */
.nav__menu-btn { list-style: none; cursor: pointer; }
.nav__menu-btn::-webkit-details-marker { display: none; }

.nav__menu-icon {
  width: var(--s-4);
  height: var(--s-4);
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: var(--hairline-w);
  vector-effect: non-scaling-stroke;
}
/* The icon answers the state: three rules closed, the middle one gone open.
   Cheaper and quieter than rotating bars into an X, and it survives at 12px.
   Two real paths and an opacity swap rather than the CSS `d:` property, which
   Firefox does not implement — this degrades nowhere. */
.nav__menu-mid { transition: opacity var(--dur-affordance) var(--ease-out); }
.nav__menu[open] .nav__menu-mid { opacity: 0; }

.nav__panel {
  position: absolute;
  inset-inline: 0;
  top: 100%;
  z-index: var(--z-nav);
  padding: var(--s-2) var(--gutter) var(--s-6);
  border-bottom: var(--hairline-w) solid var(--hairline);
  background: var(--bg);
}
.nav__panel nav { display: flex; flex-direction: column; }
.nav__panel a {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-height: var(--tap-min);
  padding-block: var(--s-3);
  border-bottom: var(--hairline-w) solid var(--hairline);
  font-family: var(--font-mono);
  font-size: var(--eyebrow);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
  transition: color var(--dur-hover) var(--ease-out);
}
.nav__panel a:last-child { border-bottom: 0; }
@media (hover: hover) and (pointer: fine) {
  .nav__panel a:hover { color: var(--fg); }
}
.nav__panel a:active { color: var(--accent); }

/* Enter. --dur-affordance, ease-out, transform+opacity only — and the panel
   opens DOWNWARD from the bar it belongs to, which is what makes the movement
   read as the bar extending rather than a sheet arriving from nowhere. Never
   from scale(0): it starts at -4px and 0 opacity, not from nothing. */
.nav__menu[open] .nav__panel {
  animation: nav-panel-in var(--dur-affordance) var(--ease-out) both;
}
@keyframes nav-panel-in {
  from { opacity: 0; transform: translateY(calc(var(--s-2) * -1)); }
  to   { opacity: 1; transform: translateY(0); }
}

@media (min-width: 900px) {
  .nav__menu { display: none; }
}
/* Zero motion means zero (rule 7): the panel still opens, it just arrives. */
@media (prefers-reduced-motion: reduce) {
  .nav__menu[open] .nav__panel { animation: none; }
  .nav__panel a { transition: none; }
  .nav__menu-mid { transition: none; }
}

/* Measured defect, Phase 9: both of these used to switch on together at 900px,
   and the nav overflowed the viewport from 900 to 941px — 41px of horizontal
   scroll at exactly the width the breakpoint fires. Logo + four links + two
   buttons measured 941px inside an 864px content box.

   Dropping the ghost button is what buys the room (114px + gap), and it is the
   right thing to drop: it is the third link to Die Jobvermittler in one
   viewport, after the hero's secondary action and before the footer's. The four
   section links are the nav's actual job and they keep their 900px entrance. */
@media (min-width: 900px) {
  .nav__links { display: flex; }
  .nav__actions { margin-inline-start: 0; }
}
@media (min-width: 1080px) {
  .nav__actions > a.btn--ghost { display: inline-flex; }
}
@media (max-width: 559px) {
  .nav__actions .btn { padding-inline: var(--s-3); }
  /* Measured at 320: logo 125 + gap 32 + actions 178 = 335 in a 280px content
     box. The gap and the menu LABEL are the two things that can go without
     losing a control — the icon keeps its 44px target and the accessible name
     stays "Menü" via aria-label, so 2.5.3 holds when the text is not painted.
     After both: 125 + 16 + 44 + 8 + 86 = 279. */
  .nav { gap: var(--s-4); }
  .nav__menu-label { display: none; }
  /* Icon-only, so --tap-min has to come from the WIDTH too — .btn--sm only
     guarantees the height and the box measured 42px across. */
  .nav__menu-btn { padding-inline: var(--s-3); min-width: var(--tap-min); justify-content: center; }
}


/* ══ 01 · HERO ══════════════════════════════════════════════════════════
   PHASE 6b — the copy came off the clip.

   Every previous version overlaid the type on full-bleed footage, which forced
   a scrim, and the scrim is what buried the machine: two opaque gradients that
   left only 8% of the frame clear. Even the measured, search-optimised ellipse
   that replaced them still had to cover 42% of the frame to hold AA.

   Side by side, that whole class of problem disappears. The copy sits on solid
   ink at 17.16:1, the clip keeps 100% of its own pixels, and there is no scrim
   on desktop at all. The clip is also smaller, which is what lets the display
   type drop from 134px to ~84px and stop shouting.

   The frame, the corner ticks and the mono readout are DIRECTION.md §1 read
   literally: a spec sheet applied to people instead of parts. */

/* Pulled up under the nav so the stage starts at viewport top.

   Measured before the fix: the nav is 80px, so the hero began at document y=80
   and its 100svh stage ran 80 -> 1160 — overflowing the viewport by exactly a
   nav-height. The scrub could not start until scrollY passed 80, so the first
   80px of scrolling slid the hero upward and moved the clip not at all. That
   dead zone inside the first screen is what a visitor reads as "a short scroll
   where nothing happens".

   The nav and the hero are both ink bands, so the overlap is seamless; the grid
   below simply pads its content clear of the nav. */
.hero {
  position: relative;
  height: 100svh;
  min-height: calc(var(--s-48) * 4);
}

.hero__stage {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
}

/* Copy left, clip right. The asymmetry is deliberate — 5fr/7fr, not 1fr/1fr,
   so the clip is the larger object and the type is the denser one. */
.hero__grid {
  display: grid;
  align-content: center;
  gap: var(--s-8);
  height: 100%;
  padding-inline: var(--gutter);
  padding-block: var(--s-16) var(--s-24);
}

/* The pull-up and the nav clearance belong ONLY to the pinned layout.

   Below 1080px the hero is static and auto-height: there is no 100svh stage,
   so it cannot overflow the viewport and there is no dead zone to fix. Applying
   the negative margin there simply slid the clip 48px behind the nav — the pin
   was gone but the margin was not. Both halves of the fix are scoped together
   so they can never drift apart again. */
@media (min-width: 1080px) {
  .hero { margin-block-start: calc(var(--nav-h) * -1); }
  /* stretch, not center. Measured before Phase 9c: the copy column ran past the
     bottom of the picture at every desktop width - 30px at 1920, 120px at 1440,
     172px at 1080 - so the buttons hung below the frame and the two columns
     were visibly not a pair. Centring cannot fix that; it only splits the
     excess above and below. Stretching makes the two columns one height by
     construction, at every width, and .hero__media below grows into whatever
     that height turns out to be. */
  .hero__grid {
    grid-template-columns: 5fr 7fr;
    align-items: stretch;
    gap: var(--s-16);
    padding-block: calc(var(--nav-h) + var(--s-6)) var(--s-12);
  }
  /* The frame is a column: picture on top, mono readout pinned under it. */
  .hero__frame { display: flex; flex-direction: column; }
  /* 1 0 auto, deliberately. aspect-ratio sets the natural 16:9 size; grow lets
     the picture fill a taller row at the narrow desktop widths where the copy
     still drives the height; NO shrink, so it can never collapse below 16:9 and
     crop the footage more than the row actually requires. */
  .hero__media { flex: 1 0 auto; min-height: 0; }
}

.hero__copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  min-width: 0;
}

/* ---- The display line -------------------------------------------------
   Two statements in ONE grid cell. Stacking them costs no layout when they
   trade places, and the taller reserves the height for both so nothing below
   shifts. Outside the motion guard this rule never applies and they fall back
   to block flow, reading as two ordinary sentences. */
.hero__display { margin-block: var(--s-6); }
.hero__display > * { margin: 0; }

.hero-live .hero__display { display: grid; }
/* align-self: start, not the default stretch — a stretched grid item reports
   the cell's height, so an unequal line would sit vertically centred in the
   reserve and drift as the lines trade places. Top-aligned, every line starts
   on the same baseline whatever it wraps to. */
.hero-live .hero__line { grid-area: 1 / 1; align-self: start; }

.hero__h {
  font-size: var(--display-hero);
  font-weight: var(--wt-semi);
  font-stretch: var(--wd-display);
  line-height: var(--lh-display);
  letter-spacing: var(--tr-display);
  text-wrap: balance;
}
.hero__h em {
  font-style: normal;
  color: var(--accent);
}
/* The slogan is not the same size in both modes, and that is deliberate.

   Inside .hero-live the two lines share ONE grid cell and only one is ever
   visible, so the slogan takes the full display size and lands on the team
   shot as the payoff. Outside it — no JS, reduced motion, or under 1080px —
   they are in normal block flow, one under the other, and a display-size
   slogan under a six-line display-size h1 stacks nine lines of 69px type
   before the reader reaches the lead. There it drops to the mid tier and
   reads as the kicker it actually is. */
.hero__line--2 {
  font-size: var(--h3-l);
  font-weight: var(--wt-medium);
  margin-top: var(--s-4);
}
.hero-live .hero__line--2 {
  font-size: var(--display-hero);
  margin-top: 0;
}

.hero__lead {
  max-width: var(--measure);
  margin-block: 0 var(--s-8);
  font-size: var(--body-l);
  line-height: var(--lh-lead);
  letter-spacing: var(--tr-lead);
  color: var(--fg-muted);
  text-wrap: pretty;
}

/* Measured defect, Phase 9: as a wrapping flex row this pair ragged by 4.6px
   on every viewport from 1080 to ~1490 — i.e. the whole common desktop range,
   1440 included. The copy column is 525px at 1440 and the two intrinsic labels
   need 542px, so they wrapped, and the second is 4.6px wider than the first, so
   the stack ended on a ragged right edge that reads as a mistake rather than a
   choice. Widening the column would have traded a hero composition for a
   button.

   A two-track auto-fit grid removes the failure mode instead of tuning around
   it: the columns are equal at every width, so side by side or stacked, the
   pair ends flush.

   The floor is 15.5rem (248px) and that number is the widest button's own
   intrinsic width, not a round figure: auto-fit will only lay two tracks when
   both fit, so a floor BELOW the widest label lets the grid make two columns
   too narrow to hold it and the label wraps inside its own box. It was 17rem
   while the secondary read "Sie suchen eine Stelle? ↗" (267px); with the label
   now "Offene Stellen" (190px) and the hero buttons on tighter side padding,
   the widest is the primary at 247px. Two tracks therefore need 508px, which
   the copy column has from about 1400px up — one row at 1440 and 1920, an
   equal-width flush stack below. */
.hero__actions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15.5rem, 1fr));
  gap: var(--s-3);
  width: 100%;
}
/* --s-4, not the --s-6 every other button uses. 16px of side padding on each
   button is what brings the primary from 263px to 247px, and 247 vs 256.5 (the
   track width at 1440) is the whole margin between one row and two. */
.hero__actions .btn { padding-inline: var(--s-4); }
/* The labels are centred only because the boxes are now stretched to a track:
   a 525px box with the label hugging its left edge reads as a background, not a
   button. Same treatment .btn--block already gets on the form submit. */
.hero__actions .btn { justify-content: center; }

/* ---- The framed clip --------------------------------------------------- */
.hero__frame {
  position: relative;
  margin: 0;
  min-width: 0;
  border: var(--hairline-w) solid var(--hairline);
}

.hero__media {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--bg-raised);
}

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

/* The clip sits ON the poster. Frame 0 of the clip IS the poster frame, so the
   handover is invisible and needs no crossfade choreography. */
.hero__video {
  opacity: 0;
  transition: opacity var(--dur-enter) var(--ease-out);
  pointer-events: none;
}
.hero__video[data-ready] { opacity: 1; }

/* THERE IS NO SCRIM ANY MORE, at any width. Nothing is written over the
   footage: the clip lives in its own bordered frame and the copy is beside
   it (or above it, below 1080px) on solid ink at 17.16:1. The hairline
   border is the seam a gradient used to fake. Removing it is what the
   side-by-side layout buys — the previous ellipse still had to cover 42%
   of the frame to hold AA. */

/* Corner ticks. Crosshair marks from the spec-sheet vocabulary, drawn with
   borders on an empty span so they cost no elements beyond the four. */
.hero__tick {
  position: absolute;
  width: var(--s-3);
  height: var(--s-3);
  border: 0 solid var(--accent);
}
.hero__tick--tl { top: calc(var(--s-2) * -1); left: calc(var(--s-2) * -1);
                  border-top-width: var(--hairline-w); border-left-width: var(--hairline-w); }
.hero__tick--tr { top: calc(var(--s-2) * -1); right: calc(var(--s-2) * -1);
                  border-top-width: var(--hairline-w); border-right-width: var(--hairline-w); }
.hero__tick--bl { bottom: calc(var(--s-2) * -1); left: calc(var(--s-2) * -1);
                  border-bottom-width: var(--hairline-w); border-left-width: var(--hairline-w); }
.hero__tick--br { bottom: calc(var(--s-2) * -1); right: calc(var(--s-2) * -1);
                  border-bottom-width: var(--hairline-w); border-right-width: var(--hairline-w); }

/* ---- Live readout ------------------------------------------------------
   Written by motion.js on the same rAF that seeks the clip. Instrumentation,
   not content: aria-hidden, because a screen reader announcing a frame counter
   on every scroll tick would be hostile. */
.hero__readout {
  display: flex;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-2) var(--s-3);
  border-top: var(--hairline-w) solid var(--hairline);
  font-family: var(--font-mono);
  font-size: var(--eyebrow);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}
.hero__beat-name { color: var(--accent); }
/* Tabular figures so the counter does not jitter as digits change width. */
.hero__timecode { font-variant-numeric: tabular-nums; }

.hero__readout { display: none; }
.hero-live .hero__readout { display: flex; }

/* ---- Scroll affordance ------------------------------------------------- */
.hero__progress {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: var(--z-content);
  height: var(--hairline-w);
  background: var(--hairline);
}
.hero__progress::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--accent);
  transform-origin: left center;
  transform: scaleX(0);
}
.hero__cue {
  position: absolute;
  inset-block-end: var(--s-6);
  inset-inline-end: var(--gutter);
  z-index: var(--z-content);
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--eyebrow);
  font-weight: var(--wt-medium);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}
/* Neither exists without the scrub track — there is nothing to indicate. */
.hero__progress, .hero__cue { display: none; }
.hero-live .hero__progress, .hero-live .hero__cue { display: block; }

/* ---- Mobile ------------------------------------------------------------
   A designed alternative, not the desktop one with the pin switched off
   (DIRECTION.md §8). The clip takes a band at the top and the copy sits below
   it on solid ground — full contrast, nothing clipped, no pin. */
@media (max-width: 1079px) {
  .hero { height: auto; min-height: 0; }
  .hero__stage { position: static; height: auto; }
  /* No nav clearance needed: without the pull-up the hero starts below the nav
     in normal flow, exactly as it should. */
  .hero__grid { padding-block: var(--s-8) var(--s-16); gap: var(--s-8); }
  .hero__frame { order: -1; }
  .hero__media { aspect-ratio: 3 / 2; }
}
@media (max-width: 899px) {
  .hero__media { aspect-ratio: 4 / 3; }
  /* The width and the centring moved into .hero__actions itself — the grid
     gives both at every width, so the phone no longer needs its own rule. */
}

/* ══ 02 · PROOF STRIP ═══════════════════════════════════════════════════
   A hairline bar hung directly off the hero, not a section with air around it —
   it should read as the hero's spec footer. Four items, mono, wrapping to two
   rows on a phone rather than scrolling. */

.proof {
  border-block: var(--hairline-w) solid var(--hairline);
  padding-block: var(--s-4);
}
.proof__list {
  display: flex;
  flex-wrap: wrap;
  /* Gap, not drawn separators, for the same reason as .fields__list: a sibling
     pseudo-element cannot know where a flex line wraps, and on a phone this row
     wraps to two lines and would orphan a rule at the start of the second. */
  gap: var(--s-3) var(--s-12);
  font-family: var(--font-mono);
  font-size: var(--eyebrow);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}
/* The one accent mark in the strip sits on the first item, so the row has a
   start point without needing a separator between every pair. */
.proof__item:first-child { color: var(--accent); }


/* ══ 03 · POSITIONING ═══════════════════════════════════════════════════
   Magenta band. The heading sits in its own column and the body in another, so
   the composition is a split rather than the eyebrow→h2→lead stack four other
   sections use. The spec-rail that used to live here is gone: registry data
   proves existence, not delivery, and it now appears once in the footer. */

.pos { padding-block: var(--section-y); }

.pos__grid {
  display: grid;
  gap: var(--s-8);
}
@media (min-width: 900px) {
  /* Phase 8: was 6fr/5fr with the 199px headline in the WIDER column and the
     287px paragraph in the narrower one — weighting inverted. Flipped to
     5fr/6fr so the paragraph gets the wider measure, and align-items: end
     bottom-aligns the two columns instead of top-aligning them, which is what
     left an 88px dangle under the shorter (headline) column. */
  .pos__grid {
    grid-template-columns: 5fr 6fr;
    gap: var(--s-16);
    align-items: end;
  }
}

.pos__h { margin-block: var(--s-4) 0; }
.pos__head .btn { margin-top: var(--s-6); }
.pos__body .body { margin-bottom: 0; }

/* On magenta the solid button would be cotton-on-cotton with the band's own
   --accent. Invert it: cotton ground, magenta label. */
[data-band="magenta"] .btn--solid {
  background: var(--cotton);
  border-color: var(--cotton);
  color: var(--magenta-deep);
}
[data-band="magenta"] .btn--solid:hover { background: var(--magenta-tint); border-color: var(--magenta-tint); }


/* ══ 04 · BENEFITS ══════════════════════════════════════════════════════
   Four statements as a numbered ladder, not four cards. Two columns so the
   section is wide and short rather than a tall stack. */

.ben { padding-block: var(--section-y); }

.ben__list {
  display: grid;
  gap: 0;
  margin-top: var(--s-12);
}
@media (min-width: 900px) {
  .ben__list { grid-template-columns: 1fr 1fr; column-gap: var(--s-16); }
}

.ben__item {
  display: flex;
  align-items: baseline;
  gap: var(--s-6);
  padding-block: var(--s-6);
  border-top: var(--hairline-w) solid var(--hairline);
}
.ben__n {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--eyebrow);
  letter-spacing: var(--tr-eyebrow);
  color: var(--accent);
}
.ben__t {
  font-size: var(--body-l);
  line-height: var(--lh-lead);
  letter-spacing: var(--tr-lead);
  text-wrap: pretty;
}


/* ══ 06 · BERUFSFELDER ══════════════════════════════════════════════════
   Was .roles — a five-item list of job titles set at --display-l. Measured,
   that rendered at 72px against a 63px h1: the least important content on the
   page was the largest type on it, on desktop and on mobile both. The list now
   sits at --h3-l (~32px at 1440), below the h2, which is where a supporting
   list belongs.

   Phase 8: `flex-wrap` with no separators measured 31.68px per field and
   rendered as a run-on sentence, four per row. Rebuilt as a real grid —
   4×2 at 1080px+, 2×4 at 640px+, single column below — using the same
   gap-as-rule technique .svc__grid used (background paints the hairline,
   the gap between cells IS the rule, no pseudo-elements needed). Field size
   dropped from 31.68px to --h3 so eight items stop competing with the
   section headline. Each cell is now a link into the enquiry form. */

.fields { padding-block: var(--section-y); }

.fields__note { max-width: 46ch; color: var(--fg-muted); }

/* Phase 9b — rebuilt as photographic cards.

   The hairline text grid this replaces was right for a section carrying a
   supporting list. It is not right for what this section now is: with #modelle
   deleted, Berufsfelder is the page's only answer to "what do you actually
   recruit for", and eight 86px cells of one word each read as a nav menu.

   The cards are the eight stills, each generated to its own trade
   (MEDIA-BRIEF.md §2). The gap-as-rule trick is dropped with the flat cells —
   dark cards separated by real space read as a set of plates, and a hairline
   drawn between two photographs is a line about nothing. */
.fields__list {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-4);
  margin-top: var(--s-12);
}
@media (min-width: 560px)  { .fields__list { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1280px) { .fields__list { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* minmax(0, 1fr) rather than 1fr throughout: a bare 1fr floors at its content's
   min-content width, and "Qualitätsmanagement" is one unbreakable 19-character
   word. One long label would otherwise widen its own column and the eight cards
   would stop being eight of the same thing. */

.fields__item {
  position: relative;
  display: flex;
  align-items: flex-end;
  aspect-ratio: 4 / 3;
  padding: var(--s-6);
  overflow: clip;
  /* The card carries data-band="ink" in the markup, so --bg/--fg/--accent
     resolve to the ink set inside it whatever band the SECTION is on. That is
     the token system doing its job rather than the component reaching past it
     for a palette value, and it means the section can move between bands
     without touching a line of this. */
  background: var(--bg);
  color: var(--fg);
  border: var(--hairline-w) solid var(--hairline);
  font-size: var(--h3);
  font-weight: var(--wt-semi);
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-heading);
  /* A card must not hyphenate mid-field — a broken "Qualitätsmanage-ment"
     reads as two trades, not one. */
  hyphens: manual;
}
/* Four across gets short and wide at 1280; 5:4 gives the picture back the
   height the label eats. Below 560 a single full-width 4:3 card is already
   tall enough. */
@media (min-width: 1280px) { .fields__item { aspect-ratio: 5 / 4; } }

.fields__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* scale, not width — the settle stays on the compositor.
     --dur-affordance (220ms), not --dur-enter (520ms): this is a RESPONSE to a
     pointer, not a scroll reveal, and a response the visitor is watching reads
     as sluggish past ~300ms. */
  transition: transform var(--dur-affordance) var(--ease-out);
}

/* The scrim, and the only reason the label is legible on eight different
   photographs. Opaque --ink at the bottom edge where the label sits, gone by
   the upper half so the picture is not flattened. Measured worst-case glyph
   contrast across all eight cards at the real rendered crop: see PROGRESS.md. */
.fields__item::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top,
    var(--bg) 0%,
    color-mix(in srgb, var(--bg) 82%, transparent) 34%,
    color-mix(in srgb, var(--bg) 30%, transparent) 62%,
    transparent 100%
  );
}
.fields__label { position: relative; z-index: var(--z-content); }

/* The accent rule is the hover affordance, not a colour change: the label is
   already cotton and has nowhere brighter to go. It draws from the inline start
   along the card's bottom edge, the same gesture .eyebrow--rule and
   .roles__item use, so the page has one way of saying "this is live". */
.fields__item .fields__rule {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  height: calc(var(--hairline-w) * 3);
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-affordance) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .fields__item:hover .fields__img,
  .fields__item:focus-visible .fields__img { transform: scale(1.04); }
  /* Gated with the image scale, not left open. On a touch screen :hover latches
     on tap and does not clear on the way back from #anfrage, so an ungated rule
     leaves one card drawn and seven blank — the page inventing a selected state
     it does not have. Keyboard focus is a separate, real state and keeps it. */
  .fields__item:hover .fields__rule { transform: scaleX(1); }
}
.fields__item:focus-visible .fields__rule { transform: scaleX(1); }

/* Press feedback. These eight cards are the largest pressable surface on the
   page and were the only ones with no answer to a press — the buttons have had
   scale(0.97) since Phase 4. 0.985 rather than 0.97 because the card is ~300px
   wide: the same ratio on a box that size reads as a lurch, not a press. */
.fields__item {
  transition: transform var(--dur-press) var(--ease-out);
}
.fields__item:active { transform: scale(0.985); }

.fields__item:focus-visible { outline-offset: calc(var(--s-2) * -1); }

/* Zero motion means zero (ARCHITECTURE-ESSENTIALS rule 7). The rule still
   appears — it carries the affordance — it just arrives without drawing. */
@media (prefers-reduced-motion: reduce) {
  .fields__img { transition: none; }
  .fields__rule { transition: none; }
  /* Zero motion is zero: the press must not scale either (rule 7). */
  .fields__item { transition: none; }
  .fields__item:active { transform: none; }
}


/* ══ 06 · COMPANY ═══════════════════════════════════════════════════════
   Phase 8: the group render (founders-*.avif) is gone. It showed René and
   Esther standing together, and the two portraits below showed them again —
   the same two people twice — while `align-items: center` on a 793px text
   column beside a 475px image left 159px of dead air above and below the
   image. The two portraits are promoted out of the text column into the
   right column itself, large and two-up; `align-items: center` is dropped so
   both columns run top-aligned and end within measurement tolerance of each
   other instead of centering a short image against a tall paragraph. */

.co { padding-block: var(--section-y); }

.co__grid { display: grid; gap: var(--s-12); }
@media (min-width: 900px) {
  /* stretch, not start: the text column (fixed content, height varies only
     with wrap) and the portrait column (aspect-ratio-driven) trade off which
     is taller as the viewport narrows and the two columns' widths change
     independently. Stretching the row lets whichever is shorter fill the
     row instead of leaving a fixed gap that only holds at one width — see
     .person__img below for the half that actually grows. */
  .co__grid { grid-template-columns: 5fr 7fr; gap: var(--s-16); align-items: stretch; }
}

.co__body .eyebrow { margin-bottom: var(--s-4); }
.co__body .h2 { margin-bottom: var(--s-6); }
.co__body .lead { margin-bottom: var(--s-4); }

/* minmax(0, 1fr), not 1fr. Measured at 320px: a bare 1fr is minmax(auto, 1fr),
   so each track floors at its content's min-content width — "Esther Christiane
   Olipitz / Geschäftsführerin" is the longer caption, and the tracks resolved
   128.7px / 145.9px. Two founder portraits at a 13% size difference is not a
   composition, it is the caption text pushing the grid around. */
.people {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-6);
}
/* Each portrait li stretches to the row height (.co__grid's stretch, then
   grid's own default align-items: stretch inside .people); the image then
   flexes to consume whatever that leaves after its own caption text. */
.person {
  display: flex;
  flex-direction: column;
  /* The reveal starts the picture at scale(1.04) (motion.css §img-reveal). A
     transform still contributes scroll overflow even while clip-path hides the
     paint, which is why a 320px viewport measured 2px of horizontal scroll
     traced to an image sitting 2.6px left of its own column. Clipping the cell
     is what the wipe wants anyway. */
  overflow: clip;
}
/* Measured after the promotion: the text column (eyebrow+h2+lead+body) runs
   ~389px at 1440, ~433px at 900; two 4:5 portraits plus their name/role
   caption ran ~486px at 1440 but only ~264px at 900 — the taller side flips
   with viewport width, because the image column's width (and so its
   aspect-ratio-driven height) shrinks faster than text reflow adds height.
   aspect-ratio sets a sane base size (4/4.2 measures 27.5px delta at 1440,
   close to the ~20px target); flex:1 with min-height:0 then lets the image
   grow past that base to fill the stretched row when text is the taller
   side instead (900px: image was 169px short before this, matches now). */
.person__img {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  aspect-ratio: 4 / 4.2;
  object-fit: cover;
  object-position: top;
  margin-bottom: var(--s-3);
  border: var(--hairline-w) solid var(--hairline);
}
.person__name {
  font-weight: var(--wt-semi);
  line-height: var(--lh-heading);
  letter-spacing: var(--tr-heading);
}
.person__role {
  font-family: var(--font-mono);
  font-size: var(--eyebrow);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}


/* ══ 07 · PROCESS ═══════════════════════════════════════════════════════
   Stacked sticky steps. Three siblings, all sticky at the nav's height, each
   opaque and each covering the previous by document order alone — no z-index,
   no state, no JS. Retired below 900px, where a phone has neither the height
   nor the scroll length for it, exactly as the references do. */

.proc { padding-block: var(--section-y); }
.proc .sec-head { margin-bottom: var(--s-12); }

.proc__step {
  padding-block: var(--s-16);
  border-top: var(--hairline-w) solid var(--hairline);
  background: var(--bg);
}

@media (min-width: 900px) {
  .proc__step {
    position: sticky;
    top: calc(var(--s-16) + var(--s-2));
    min-height: calc(var(--s-48) * 2);
    display: flex;
    align-items: center;
  }
}

.proc__inner {
  display: grid;
  gap: var(--s-6);
}
@media (min-width: 720px) {
  .proc__inner {
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--s-12);
  }
  /* Below the three-column layout the picture is a band under the step, not a
     column beside it - spanning both tracks keeps it off the 17px number
     column, where auto sizing would have squeezed it to nothing. */
  .proc__fig { grid-column: 1 / -1; }
}
@media (min-width: 1080px) {
  /* 24rem, a fixed track rather than an fr: the three steps are read as a
     stack, and a picture that changed width from step to step would read as
     three different treatments. The text keeps --measure regardless, so the fr
     column only ever adds air, never line length. */
  .proc__inner {
    grid-template-columns: auto minmax(0, 1fr) 24rem;
    align-items: stretch;
  }
  .proc__fig { grid-column: auto; }
}

/* The step is a fixed band and the source is 4:5, so the crop is the step's,
   not the file's - object-fit does the work and the picture always ends flush
   with the text block it belongs to. */
.proc__fig {
  margin: 0;
  overflow: clip;      /* contains the reveal's 4% overscale, as .person does */
  border: var(--hairline-w) solid var(--hairline);
}
.proc__fig img {
  width: 100%;
  height: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
}
@media (min-width: 1080px) {
  .proc__fig img { aspect-ratio: 4 / 5; }
}

.proc__n {
  font-family: var(--font-mono);
  font-size: var(--eyebrow);
  letter-spacing: var(--tr-eyebrow);
  color: var(--accent);
}
.proc__h {
  margin-bottom: var(--s-4);
  font-size: var(--display-l);
  font-weight: var(--wt-semi);
  font-stretch: var(--wd-display);
  line-height: var(--lh-display);
  letter-spacing: var(--tr-display);
}
.proc__text .lead { color: var(--fg-muted); }


/* ══ 10 · JOBVERMITTLER ═════════════════════════════════════════════════
   The second magenta band, bookending the positioning statement. Candidate
   intent lives here and nowhere else in the body — one band, one exit. */

.jv { padding-block: var(--section-y); }

.jv__grid {
  display: grid;
  gap: var(--s-8);
}
@media (min-width: 900px) {
  .jv__grid {
    grid-template-columns: 7fr 5fr;
    gap: var(--s-16);
    align-items: end;
  }
}
.jv__body .body { margin-bottom: var(--s-8); max-width: var(--measure); }
/* The domain is a single unbroken token wider than a phone column. */
.jv .h2 { overflow-wrap: break-word; }


/* ══ 11 · ANFRAGE ═══════════════════════════════════════════════════════ */

.enq { padding-block: var(--section-y); }

.enq__grid { display: grid; gap: var(--s-12); }
@media (min-width: 900px) {
  /* Phase 8: was align-items: start with the figure sized purely by its own
     aspect ratio, independent of the form's height — measured 181px shorter
     than the form column. align-items: stretch makes both columns fill the
     row's height (which the taller .form column defines); inside the now
     flex-column .enq__intro, .enq__fig grows to consume exactly the space
     left after the intro text, so the two columns' bottom edges match. */
  .enq__grid { grid-template-columns: 5fr 7fr; gap: var(--s-16); align-items: stretch; }
}

.enq__intro .eyebrow { margin-bottom: var(--s-4); }
.enq__intro .h2 { margin-bottom: var(--s-6); }

.enq__direct {
  margin-top: var(--s-12);
  border-top: var(--hairline-w) solid var(--hairline);
}
.enq__direct li {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  padding-block: var(--s-4);
  border-bottom: var(--hairline-w) solid var(--hairline);
}
.enq__direct-k {
  font-family: var(--font-mono);
  font-size: var(--eyebrow);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}
.enq__direct-v {
  /* Was an 18px-tall target on a phone. The telephone number is one of the two
     ways a visitor actually converts and it was the smallest hit box on the
     page; inline-flex + --tap-min gives it a real target without changing how
     it looks. */
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  font-size: var(--body-l);
  align-self: flex-start;
  /* Same reason as .ft__contact a: the address wraps on a phone and a border
     would draw one rule across the full box. */
  text-decoration: underline;
  text-decoration-color: var(--hairline);
  text-decoration-thickness: var(--hairline-w);
  text-underline-offset: 0.25em;
  transition: color var(--dur-hover) var(--ease-out),
              text-decoration-color var(--dur-hover) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .enq__direct-v:hover { color: var(--accent); text-decoration-color: var(--accent); }
}

/* Phase 9c - the figure is a grid item, not a block inside .enq__intro, and it
   is PORTRAIT at the widths where the room exists.

   The clip is 9:16 (720x1280). In the old landscape box object-fit threw away
   68% of every frame - measured at exactly 32% of the clip visible at every
   width - which is why the video was indistinguishable from its own poster. It
   could not simply be un-cropped in place: a native 9:16 figure at the old
   column width would have been 933px tall inside an 898px row.

   As its own column it fits: 299x531 at 1440, 408x726 at 1900, and shows the
   whole frame. `overflow: clip` still contains the reveal's 4% overscale, same
   as .person. */
.enq__fig {
  position: relative;
  margin-top: var(--s-8);
  overflow: clip;
  /* start, NOT the grid default stretch, and this is not a preference.

     .enq__grid is align-items: stretch, which hands every item a DEFINITE
     block size. Per CSS Box Sizing 4, an item with a preferred aspect-ratio and
     a definite size in one axis derives the OTHER axis from the ratio and stops
     honouring stretch alignment there - so the figure was computing its width
     from the row height (714 x 9/16 = 401.6px) and overflowing its own 299px
     track. Measured exactly that before this line. Top-aligned, the width comes
     from the track and the height from the ratio, which is the way round the
     aspect-ratio was declared for. */
  align-self: start;
}

@media (min-width: 900px) {
  /* Two columns: the figure drops under the intro rather than beside the form,
     and 3:4 keeps it from eating the row at a width that cannot spare it. */
  .enq__fig { grid-column: 1; aspect-ratio: 3 / 4; margin-top: 0; }
}
@media (min-width: 1280px) {
  /* Three columns: details | the window | the form. 4fr is what makes the 9:16
     clip land inside the row height instead of setting it. */
  .enq__grid { grid-template-columns: 5fr 4fr 7fr; }
  /* No aspect-ratio here, and align-self back to stretch: as its own column the
     figure should end flush with the details and the form, the same argument
     the hero grid was rebuilt on. Filling a 299x714 cell from a 9:16 source
     crops 25% off the WIDTH and keeps 100% of the height - which is the axis a
     dolly-through-a-lobby actually lives on, and still a long way from the 32%
     of frame the old landscape box was showing. The aspect-ratio/stretch
     conflict documented above cannot bite here because there is no ratio. */
  .enq__fig { grid-column: auto; aspect-ratio: auto; align-self: stretch; }
}

/* The video sits ON the still, not instead of it. motion.js assigns the src and
   sets data-live only once the gate passes; until then the element has no src
   at all (preload="none"), so a phone, a reduced-motion visitor, a metered
   connection and a no-JS visitor all download the AVIF and nothing else. */
.enq__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-enter) var(--ease-out);
}
.enq__video[data-live] { opacity: 1; }

/* z-index, and it is not belt-and-braces - without it the poster covers the
   clip. .enq__still carries the img-reveal animation, whose end state is
   `clip-path: inset(0px 0px 0%)`. That is not `none`, so the <img> creates a
   stacking context and paints in the positioned-descendants step alongside the
   absolutely positioned <video> - and being LATER in DOM order at the same
   effective z-index, it wins. Measured: clip-path inset(0px 0px 0%) on the
   still, z-index auto on both. The video has to be raised explicitly. */
.enq__video { z-index: 1; }
.enq__still { z-index: 0; }
/* The hairline is on the FIGURE, not the image: the <video> is absolutely
   positioned over the <img>, so a border on the image would be painted on top
   of and vanish behind the live clip. */
.enq__fig { border: var(--hairline-w) solid var(--hairline); }
.enq__fig img { width: 100%; height: 100%; object-fit: cover; }


/* ---- Form ---------------------------------------------------------------- */

.form {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  padding: var(--s-8);
  border: var(--hairline-w) solid var(--hairline);
  background: var(--bg-raised);
}

.form__row { display: grid; gap: var(--s-4); }
@media (min-width: 560px) {
  .form__row { grid-template-columns: 1fr 1fr; }
  .form__row--three { grid-template-columns: repeat(3, 1fr); }
}

.field { display: flex; flex-direction: column; gap: var(--s-2); }
.field label {
  font-family: var(--font-mono);
  font-size: var(--eyebrow);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}
.field abbr { color: var(--accent); }

.field input,
.field textarea {
  width: 100%;
  padding: var(--s-3);
  /* Measured 1.37:1 at --hairline against --bg-raised — a form control whose
     only boundary a low-vision visitor cannot find, and this form is the one
     thing on the page that converts. --control-edge is 3.39:1 minimum on every
     band (WCAG 1.4.11). */
  border: var(--hairline-w) solid var(--control-edge);
  background: var(--bg);
  color: var(--fg);
  transition: border-color var(--dur-hover) var(--ease-out);
}
/* Measured: the date control came out 54.4px against 52.4 for the two text
   inputs beside it in the same three-column row — a 2px step, visible as a
   ragged edge across the row. The extra height comes from the native
   datetime-edit shadow tree asserting its own intrinsic size, which no amount
   of padding on the host will out-argue.

   So the box is stated instead of inferred. Every value is the same token the
   padded box already resolves from, so this tracks --body, --s-3 and
   --hairline-w rather than freezing a pixel number beside them: it is the
   arithmetic border-box was already doing, written down. textarea keeps auto
   height — it is the one control that is meant to grow. */
.field input {
  block-size: calc(var(--body) * var(--lh-body) + var(--s-3) * 2 + var(--hairline-w) * 2);
}

.field textarea { resize: vertical; }
.field input::placeholder,
.field textarea::placeholder { color: var(--fg-muted); }
.field input:focus,
.field textarea:focus { border-color: var(--accent); }

.form .btn { margin-top: var(--s-4); }
.form__note {
  font-family: var(--font-mono);
  font-size: var(--eyebrow);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}
/* The prototype notice reuses this slot, and a three-line German sentence set
   in 11px mono caps at 0.18em tracking is not readable — the caps treatment is
   correct for a four-word legend and wrong for a sentence. The notice drops
   back to the body face and takes the accent, because it is now the answer to
   an action the visitor just took rather than a standing legend. */
.form__note[data-state="notice"] {
  font-family: var(--font-sans);
  font-size: var(--small);
  letter-spacing: var(--tr-body);
  text-transform: none;
  line-height: var(--lh-lead);
  color: var(--fg);
  border-inline-start: calc(var(--hairline-w) * 3) solid var(--accent);
  padding-inline-start: var(--s-3);
}


/* ══ 09 · FOOTER ════════════════════════════════════════════════════════ */

/* Phase 8: the ghosted team-photo overlay (.ft__bg, 34% opacity masked to the
   top edge) is removed. It read as an artefact across the footer's top edge
   and — separately — never loaded in-viewport (`currentSrc: ""`) despite the
   file being valid. If the media brief's footer asset lands, it comes back as
   a designed band with its own contrast budget, not a photo dimmed and hoped
   over. */
/* The padding is the picture. .ft__bg fills the whole footer, .ft__panel sits
   opaque in the middle of it, and this padding is the band of photograph left
   visible above and below the panel - so the number here is a composition
   decision, not spacing housekeeping. */
.ft {
  position: relative;
  overflow: clip;          /* contains the drifting .ft__bg */
  isolation: isolate;      /* one stacking context, so z-index stays local */
  /* No top padding: the picture is the footer's first element and it is
     full-bleed, so the section opens ON it. */
  padding-block: 0 var(--s-16);
}

/* Phase 9d - the picture is a band you scroll THROUGH, and the footer arrives
   after it.

   It was a ground behind an opaque panel until the user asked for the footer to
   appear only once the viewport has reached the bottom of the image. That is a
   different mechanic, not a tuning: the image is now an in-flow band with real
   height, and the footer content follows it on flat ink instead of sitting on
   top of it. Nothing overlaps, so the panel's own background and border are
   gone with the overlap that needed them.

   Two consequences worth recording, both of which used to need working around:
   the contrast question does not arise at all (no text is anywhere near the
   picture), and the inline oversize that Phase 9c needed is gone too - a band
   this tall is proportionally WIDER than the 16:9 source at desktop widths, so
   `cover` already crops on the block axis and object-position simply works.

   The drift itself is GSAP ScrollTrigger in src/js/motion.js, on the user's
   instruction. That is why there is no animation declared here. */
.ft__band {
  position: relative;
  height: min(76svh, 700px);
  overflow: clip;
}
.ft__bg {
  position: absolute;
  inset-inline: 0;
  /* 140/-20 is the parallax budget, and it is arithmetic rather than taste.
     The image spans the band from -20% to +120%; the tween moves it +/-13% of
     its OWN height, which is +/-18.2% of the band. Worst case the top edge
     lands at -1.8% and the bottom at +101.8% - inside on both, with a little
     margin, at every scroll position and viewport height.

     It was 124/-12 with a +/-8% tween, which measured **97px of travel across a
     619px band - 15.7%**, and read as no parallax at all. This is 36.4% of the
     band, about 225px. */
  top: -20%;
  height: 140%;
}
.ft__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The band is taller than it is proportionally wide relative to the source,
     so this crops on the block axis - it picks which part of the hall you see,
     and the standing group is what it should be. */
  object-position: center 56%;
}
/* A wash, not a scrim - no text is read against it, so this is compositional
   and not a contrast measure. Heavier at the foot, where the band hands over to
   the footer proper, so the picture closes down into the ink rather than
   stopping at a line. */
.ft__band::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--bg) 22%, transparent) 0%,
    color-mix(in srgb, var(--bg) 34%, transparent) 58%,
    var(--bg) 100%
  );
}

/* Ordinary footer content on the ink band. It was an opaque inset panel while
   it sat on the photograph; with the band above it instead, a bordered block on
   a ground of the same colour would be a box drawn around nothing. */
.ft__panel { position: relative; padding-block: var(--s-24) 0; }
@media (max-width: 719px) {
  .ft__band { height: min(52svh, 420px); }
  .ft__panel { padding-block: var(--s-16) 0; }
}

.ft__grid { display: grid; gap: var(--s-12); }
@media (min-width: 720px)  { .ft__grid { grid-template-columns: 1fr 1fr; } }
/* 2fr 1fr 1fr 1fr, not 2fr 1fr 1fr 1.4fr. Each .ft__h draws a hairline the full
   width of its own column, so the three of them sat at 227 / 227 / 319px — the
   Links rule visibly 40% longer than its neighbours with nothing in that column
   asking for the room. Three equal columns, three equal rules. */
@media (min-width: 1080px) { .ft__grid { grid-template-columns: 2fr 1fr 1fr 1fr; gap: var(--s-8); } }

/* 2.6 -> 1.7, same reasoning as the nav. 250x61 against an 11px slogan was the
   largest thing in the footer by a wide margin; 163x40 sits with the column
   headings instead of shouting over them. Still the larger of the two
   instances, which is right - the footer is where the mark signs off. */
.ft__logo {
  width: calc(var(--s-24) * 1.7);
  max-width: 70vw;
  height: auto;
  color: var(--fg);
}
.ft__slogan {
  margin-top: var(--s-4);
  font-family: var(--font-mono);
  font-size: var(--eyebrow);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--accent);
}

.ft__h {
  margin-bottom: var(--s-4);
  padding-top: var(--s-3);
  border-top: var(--hairline-w) solid var(--hairline);
  font-family: var(--font-mono);
  font-weight: var(--wt-medium);
  font-size: var(--eyebrow);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}

.ft__addr {
  font-style: normal;
  line-height: var(--lh-lead);
  color: var(--fg-muted);
}
.ft__contact { margin-top: var(--s-4); line-height: var(--lh-lead); }
/* text-decoration, not border-bottom. The e-mail address wraps to two lines in
   a 228px column, and a border draws one rule across the full box — so the
   underline ran the whole column width under a half-width second line. An
   underline follows each line box; a border never can.
   min-height still guarantees the 44px target. */
.ft__contact a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  text-decoration: underline;
  text-decoration-color: var(--hairline);
  text-decoration-thickness: var(--hairline-w);
  text-underline-offset: 0.25em;
  transition: color var(--dur-hover) var(--ease-out),
              text-decoration-color var(--dur-hover) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .ft__contact a:hover { color: var(--accent); text-decoration-color: var(--accent); }
}

/* Footer link list. The tap target is the whole row, not the glyph height —
   these were 18px boxes on a phone, which is the smallest thing on the page
   and one of the two ways a visitor actually converts. */
.ft__links { line-height: var(--lh-lead); }
.ft__links a {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: var(--tap-min);
  color: var(--fg-muted);
  transition: color var(--dur-hover) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .ft__links a:hover { color: var(--accent); }
}

.ft__legal {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--s-4);
  margin-top: var(--s-24);
  padding-top: var(--s-6);
  border-top: var(--hairline-w) solid var(--hairline);
  font-family: var(--font-mono);
  font-size: var(--eyebrow);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}
.ft__dot { padding-inline: var(--s-2); }
