/* ==========================================================================
   PDSITES — «LA SEÑAL»
   Direction: PDSites/Process/design-direction.md   Facts: PDSites/Brief.md

   THE ONE RULE THIS FILE EXISTS TO ENFORCE
   ----------------------------------------
   The card's cyan (#00D9FF) is 11.9:1 on the card's near-black and 1.7:1 on
   paper. So the site has TWO grounds and only two, in a fixed order —
   noche → papel → noche — and:

        --senal  (#00D9FF)  may carry text on --noche  and nowhere else.
        --senal-tinta (#0A6E8A) is the paper equivalent. 5.4:1.

   That is the whole answer to Diego's "it can't tire the eye when reading it":
   the neon holds the two ends, where the reading is short and structured, and
   the long prose happens in daylight in between.

   THE MOTION, AND WHY THERE IS MORE OF IT NOW
   -------------------------------------------
   Diego, 2026-07-31: "Could you make some cool minimalistic animations when
   scrolling down? this way the clients can have a peak of what we can do."
   So the motion is a work sample, not decoration, and it obeys three rules:

     1 · Scroll-driven in CSS first (`animation-timeline`), JS only as fallback.
     2 · Every effect is a REVEAL — it plays once and stops. Nothing loops,
         nothing hijacks the scroll, nothing scrubs. Those are rejected
         verdicts in Playbook/Preferences.md, not open questions.
     3 · `prefers-reduced-motion: reduce` removes all of it, and the page is
         complete without a single one of them.

   Effects used, by catalog number (03-effects-catalog.md):
     2.3 scroll progress rail · 2.4 reveal fallback · 2.5 hero parallax
     2.6 native scroll-driven · 5.4 clip-path wipe (the map) · 6.1 split-line
     reveal · 6.7 count-ups · 7.1 pop-up buttons · 7.3 animated nav underline
     8.1 sticky header morph · 8.4 mobile fullscreen menu · 9.3 sticky figure
     narrative · 12.2 floating labels
   ========================================================================== */

/* ── 1 · reset ─────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
img, picture, svg { display: block; max-width: 100%; }
button, input, select { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
:where(a) { color: inherit; }

/* ── 2 · fonts — self-hosted, latin subset, 2 files, 72.2 KB ───────────── */
@font-face {
  font-family: "Sora";
  src: url("assets/fonts/sora-latin-200-normal.woff2") format("woff2");
  font-weight: 200 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Sans";
  src: url("assets/fonts/ibm-plex-sans-latin-400-normal.woff2") format("woff2");
  font-weight: 400 600; font-style: normal; font-display: swap;
}
/* Russian. The `unicode-range` gate means a Spanish, English, Dutch or
   Norwegian visitor never downloads this file — it costs them zero bytes.
   Sora has no Cyrillic at all, which is why the display stack below falls
   through to IBM Plex Sans: the browser does that per-glyph, automatically. */
@font-face {
  font-family: "IBM Plex Sans";
  src: url("assets/fonts/ibm-plex-sans-cyrillic-400-normal.woff2") format("woff2");
  font-weight: 400 600; font-style: normal; font-display: swap;
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

/* ── 3 · tokens ────────────────────────────────────────────────────────── */
:root {
  /* grounds — both values declared by the business card's own CSS */
  --noche:       #05070A;
  --noche-2:     #0A0F16;   /* one step up, for the card hairline fills */
  --papel:       #F5F7F9;
  --papel-2:     #FFFFFF;

  /* ink */
  --tinta:       #0E1620;   /* 15.6:1 on --papel */
  --gris:        #5A6672;   /*  6.2:1 on --papel */
  --tinta-dark:  #E7EAEE;   /* 16.7:1 on --noche — the card's own text colour */
  --gris-dark:   #97A1AC;   /*  7.3:1 on --noche */

  /* the signal */
  --senal:       #00D9FF;   /* 11.9:1 on --noche · 1.7:1 on --papel — NEVER text on paper */
  --senal-tinta: #0A6E8A;   /*  5.4:1 on --papel — the paper equivalent */
  --senal-glow:  rgba(0, 217, 255, .55);

  /* hairlines */
  --linea:       #DDE3E8;
  --linea-dark:  #2A313A;

  /* "IBM Plex Sans" sits inside the display stack on purpose: it is what
     renders the Cyrillic that Sora does not draw. */
  --display: "Sora", "IBM Plex Sans", system-ui, sans-serif;
  --body:    "IBM Plex Sans", system-ui, sans-serif;

  /* 1080 → 1280. Pere, 2026-07-30: «demasiado espacio en blanco en laterales,
     ocupar más beneficiosamente la página entera.» At 1440 the old measure left
     180 px of dead margin each side; this leaves 80. The spacing HIERARCHY is
     untouched — only the outer bound moved, so every internal gap keeps its
     ratio to the others. Paragraphs are still capped at 62ch (§4) so the extra
     width goes to the multi-column layouts, never to line length. */
  --measure: 1280px;
  --gutter:  clamp(1.25rem, 5vw, 3rem);
  --radius:  0;              /* the card is a rectangle. So is everything here. */
  --bleed:   clamp(4.5rem, 11vh, 8rem);
  --ease:    cubic-bezier(.2, .7, .3, 1);
}

/* ── 4 · base ──────────────────────────────────────────────────────────── */
body {
  background: var(--papel);
  color: var(--tinta);
  font-family: var(--body);
  font-size: clamp(1rem, .96rem + .18vw, 1.0625rem);
  line-height: 1.65;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
h1, h2, h3 { font-family: var(--display); font-weight: 300; line-height: 1.1;
             letter-spacing: -.015em; text-wrap: balance; }
p { text-wrap: pretty; }
:where(p, dd) { max-width: 62ch; }

.wrap { width: min(100% - (var(--gutter) * 2), var(--measure)); margin-inline: auto; }

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--senal); color: var(--noche); padding: .75rem 1.25rem; font-weight: 600;
}
.skip:focus { left: 0; }

:focus-visible { outline: 2px solid var(--senal-tinta); outline-offset: 3px; }
.act--night :focus-visible, .hero :focus-visible, .phead :focus-visible,
.donde :focus-visible, .foot :focus-visible, .hdr :focus-visible { outline-color: var(--senal); }

/* ── 5 · the motif: «el filete» ────────────────────────────────────────
   The 70x2px rule the card draws under its own wordmark. The only ornament
   in the site. One per section head, and drawn ONCE on entry — never looping,
   never anywhere else. */
.rule { display: block; width: 70px; height: 2px; background: var(--senal-tinta);
        margin: 1.15rem 0 1.5rem; transform-origin: left center; }
.rule--glow { background: var(--senal); box-shadow: 0 0 10px var(--senal-glow); }

.eyebrow { font-family: var(--display); font-size: .75rem; font-weight: 600;
           letter-spacing: .22em; text-transform: uppercase; color: var(--senal-tinta);
           display: flex; align-items: baseline; }
.eyebrow--dark { color: var(--senal); }

/* The section index — «01 │ QUÉ HACEMOS». It is drawn from the `data-n`
   attribute by a pseudo-element and NOT written into the element's text, for
   one hard reason: setLang() replaces textContent when the language changes,
   which would wipe any child span. A pseudo-element survives that. It is also
   the right call semantically — the numeral is a wayfinding ornament, not
   content, and no screen reader should read "zero three" before the heading. */
.eyebrow[data-n]::before {
  content: attr(data-n);
  margin-right: .8rem; padding-right: .8rem;
  border-right: 1px solid currentColor;
  font-weight: 400; opacity: .5; letter-spacing: .1em;
}

.h2 { font-size: clamp(1.9rem, 1.35rem + 2.3vw, 3.15rem); max-width: 20ch; }
.h2--dark { color: var(--tinta-dark); }

/* A section head split in two: the heading on the left, the paragraph that
   answers it on the right. Added after the 1440px render showed the right
   half of two sections rendering as dead space — Diego's own recorded defect,
   "a two-column section whose second column is empty on desktop." Below 900px
   it is simply one column, in source order. */
.head--split { display: grid; gap: 1.25rem 4rem; margin-bottom: 3rem; }
.head--split > div > .h2 { margin-bottom: 0; }
@media (min-width: 900px) {
  .head--split { grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); align-items: end; }
  .head--split > p { padding-bottom: .4rem; }
}
.lead--p { max-width: 60ch; color: var(--gris); font-size: 1.0625rem; }

/* ══════════════════════════════════════════════════════════════════════════
   5b · THE DRAWN LAYER — «el plano»
   Diego, 2026-07-31: "The website is too plain, I know I said minimalistic but
   I need some minimalistic design throughout the website."

   The answer is one idea applied everywhere, not a pile of ornaments: the page
   is set out like a TECHNICAL DRAWING. A studio that sells a written standard
   and automated verification should look like a spec sheet, and the mark it
   was built from already has a circuit board drawn inside the D.

   Two devices remain (a third, the hairline column grid, was removed on
   2026-07-30 at Pere's instruction — see the note below):
     · a numbered index on every section head  (§5, .eyebrow[data-n])
     · corner brackets on anything with a border — the registration marks of a
       drawing, and the reason a plain box now reads as a placed object

   All of it is CSS. It adds no request, no image and no byte of JavaScript.
   ══════════════════════════════════════════════════════════════════════════ */
/* ⚠️ THE COLUMN GRID IS GONE. Pere, 2026-07-30: «me he dado cuenta que hay
   líneas a través de toda la web, quítalas.» It was the hairline six-column
   plan drawn behind every band, and it was the most visible of the three
   devices — so «el plano» is now carried by the other two only: the numbered
   index on each section head and the corner brackets.

   Do NOT reinstate it without asking. The reason it existed (Diego, 07-31:
   "too plain") is now answered by MOTION instead — the logo's entry, the
   section reveals and the hover states — which is the trade Pere asked for in
   the same message. Adding line-work back would re-break his instruction.

   `position: relative` stays: .hero__field, .hero__scroll and the phead
   ornaments are all absolutely positioned against these blocks. */
.act, .hero, .phead { position: relative; }

/* corner brackets. Two pseudo-elements, so two opposite corners: enough to
   read as a registration mark, and half the ink of four. */
.card, .terms, .reserved__slot { position: relative; }
.card::before, .terms::before, .reserved__slot::before {
  content: ""; position: absolute; top: -1px; left: -1px; width: 11px; height: 11px;
  border-top: 1px solid var(--senal-tinta); border-left: 1px solid var(--senal-tinta);
}
.card::after, .terms::after, .reserved__slot::after {
  content: ""; position: absolute; right: -1px; bottom: -1px; width: 11px; height: 11px;
  border-bottom: 1px solid var(--senal-tinta); border-right: 1px solid var(--senal-tinta);
}
/* the org cards already spend ::before on the chart's riser, so they take one
   corner rather than two — a full set would have meant redrawing the chart */
.org__person::after {
  content: ""; position: absolute; right: -1px; bottom: -1px; width: 11px; height: 11px;
  border-bottom: 1px solid var(--senal-tinta); border-right: 1px solid var(--senal-tinta);
}
/* on the map they sit at the TOP corners: the bottom-right is where the plate
   lives, and `overflow: clip` on the frame eats anything hung outside it */
.map__frame::before, .map__frame::after {
  content: ""; position: absolute; top: 0; width: 14px; height: 14px; z-index: 3;
  border-top: 1px solid var(--senal);
}
.map__frame::before { left: 0;  border-left: 1px solid var(--senal); }
.map__frame::after  { right: 0; border-right: 1px solid var(--senal); }

/* ── 6 · buttons — 7.1 pop-up ──────────────────────────────────────────── */
.btn {
  --btn-bg: var(--senal); --btn-fg: var(--noche);
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 48px; padding: .8rem 1.6rem;
  background: var(--btn-bg); color: var(--btn-fg);
  font-family: var(--display); font-weight: 600; font-size: .92rem;
  letter-spacing: .04em; text-decoration: none; border: 1px solid var(--btn-bg);
  transition: transform .28s var(--ease), box-shadow .28s ease, background .2s ease;
}
.btn:hover { transform: translateY(-3px); box-shadow: 0 12px 30px rgba(0, 217, 255, .28); }
.btn:active { transform: translateY(-1px); }
.btn--lg { padding: 1rem 2rem; font-size: 1rem; }
.btn--sm { min-height: 44px; padding: .5rem 1.05rem; font-size: .82rem; }  /* 44 = the thumb floor */
.btn--ghost { --btn-bg: transparent; --btn-fg: var(--tinta-dark); border-color: var(--linea-dark); }
.btn--ghost:hover { --btn-fg: var(--senal); border-color: var(--senal); box-shadow: none; }
/* on paper the fill has to be the passing cyan, not the neon */
.act--paper .btn { --btn-bg: var(--senal-tinta); --btn-fg: #fff; }
.act--paper .btn:hover { box-shadow: 0 12px 30px rgba(10, 110, 138, .25); }

/* ── 7 · 2.3 the scroll progress rail ──────────────────────────────────
   Two pixels of cyan across the top, driven by the document's own scroll
   position in pure CSS. Where a browser has no scroll-driven animations it
   stays at scale 0 and is simply never seen — there is nothing to fall back
   to and nothing to fix. */
.progress { position: fixed; inset: 0 0 auto 0; height: 2px; z-index: 60; pointer-events: none; }
.progress span { display: block; height: 100%; background: var(--senal);
                 box-shadow: 0 0 12px var(--senal-glow);
                 transform-origin: 0 50%; scale: 0 1; }
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .progress span { animation: prog linear both; animation-timeline: scroll(root block); }
  }
}
@keyframes prog { from { scale: 0 1; } to { scale: 1 1; } }

/* ── 8 · header — 8.1 transparent → solid · 8.4 mobile panel ───────────── */
.hdr {
  position: fixed; inset: 0 0 auto 0; z-index: 40;
  display: flex; align-items: center; gap: clamp(1rem, 3vw, 2.5rem);
  padding: 1rem var(--gutter);
  background: transparent; border-bottom: 1px solid transparent;
  transition: background .45s ease, padding .45s ease, border-color .45s ease;
}
.hdr.is-stuck {
  background: rgba(5, 7, 10, .92); border-bottom-color: var(--linea-dark);
  padding-block: .55rem;
}
@supports (backdrop-filter: blur(12px)) {
  /* only where there is no moving footage underneath — there is none here */
  .hdr.is-stuck { background: rgba(5, 7, 10, .78); backdrop-filter: blur(12px); }
  /* ⚠️ backdrop-filter makes an element a containing block for its
     position:fixed descendants, which would shrink the full-screen menu panel
     down to the height of the header bar. So the blur comes OFF for as long
     as the panel is open — main.js sets .is-menu. Do not delete this. */
  .hdr.is-menu.is-stuck { backdrop-filter: none; background: rgba(5, 7, 10, .97); }
}
/* the mark is 34px tall by design; the LINK around it is padded out to 44 so
   it is still a thumb-sized target — phone-fit.py flagged it at 46x34 */
.hdr__mark { display: flex; align-items: center; min-height: 44px; padding-inline: 2px; }
.hdr__mark img { width: auto; height: 34px; }

.hdr__nav { display: flex; gap: clamp(.9rem, 1.7vw, 1.6rem); margin-left: auto; }
/* 7.3 animated underline — a scaled rule, not a border, so it draws from the
   left instead of appearing all at once */
.hdr__nav a {
  position: relative; white-space: nowrap;   /* a nav item that wraps to two
     lines reads as two items — caught in the 1440px render */
  font-family: var(--display); font-size: .82rem; font-weight: 400; letter-spacing: .05em;
  color: var(--tinta-dark); text-decoration: none;
  /* .72rem, not .4rem: the link box measured 34 px tall, under the 44 px thumb
     floor. A 1440 px viewport can still be a touchscreen, and the house rule
     carries no exception for wide ones. The underline below is pinned near the
     text rather than to the box, so the extra height stays invisible. */
  padding: .72rem 0;
  transition: color .2s;
}
/* the panel's own call to action: it exists only inside the open mobile menu,
   because the header button is hidden there to keep the bar one row tall */
.nav__cta { display: none; }
.hdr__nav a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: .5rem; height: 1px;
  background: var(--senal); scale: 0 1; transform-origin: left center;
  transition: scale .32s var(--ease);
}
.hdr__nav a:hover { color: var(--senal); }
.hdr__nav a:hover::after { scale: 1 1; }
.hdr__nav a[aria-current="page"] { color: var(--senal); }
.hdr__nav a[aria-current="page"]::after { scale: 1 1; }

.hdr__end { display: flex; align-items: center; gap: .75rem; }

/* the burger. Hidden unless JS is running AND the viewport is narrow — with
   no JS the nav below simply wraps onto its own row and every page stays
   reachable, which is the whole reason it is built this way round. */
.hdr__burger { display: none; align-items: center; gap: .55rem;
               min-height: 44px; padding: 0 .35rem; margin-left: auto;
               font-family: var(--display); font-size: .8rem; font-weight: 500;
               letter-spacing: .12em; text-transform: uppercase; color: var(--tinta-dark); }
.hdr__bars { display: grid; gap: 5px; width: 22px; }
.hdr__bars i { display: block; height: 1px; background: currentColor;
               transition: translate .3s var(--ease), rotate .3s var(--ease), width .3s var(--ease); }
.hdr__burger[aria-expanded="true"] { color: var(--senal); }
.hdr__burger[aria-expanded="true"] .hdr__bars i:first-child { translate: 0 3px; rotate: 45deg; }
.hdr__burger[aria-expanded="true"] .hdr__bars i:last-child  { translate: 0 -3px; rotate: -45deg; }

/* language — a real <select>: works on every phone, with a keyboard and with a
   screen reader, and with JS off it simply shows Español and does nothing. */
.lang { position: relative; display: flex; }
.lang__lbl {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
.lang select {
  appearance: none; -webkit-appearance: none;
  min-height: 44px; padding: 0 2rem 0 .85rem;
  background: transparent; border: 1px solid var(--linea-dark); color: var(--tinta-dark);
  font-family: var(--display); font-size: .8rem; font-weight: 500; letter-spacing: .06em;
  cursor: pointer; border-radius: 0; transition: color .2s, border-color .2s;
}
.lang select:hover, .lang select:focus { color: var(--senal); border-color: var(--senal); }
.lang::after {
  content: ""; position: absolute; right: .85rem; top: 50%; translate: 0 -60%;
  width: 7px; height: 7px; border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor; rotate: 45deg;
  color: var(--gris-dark); pointer-events: none;
}
.lang select option { background: var(--noche); color: var(--tinta-dark); }

/* 900, down from 1280. Diego, 2026-07-31: "the menu of the website is too long,
   it occupies all of the width." The bar carried seven items; it now carries
   four — Qué hacemos · El sistema · Quiénes somos · Trabajo — and the full
   section index moved to the footer, where a long list belongs. Four items fit
   from about 810 px, so the panel is only needed on phones and small tablets. */
@media (max-width: 900px) {
  .hdr { flex-wrap: wrap; gap: .75rem; }
  .hdr__mark { margin-right: auto; }
  /* one row on the bar: logo · language · burger. The call to action moves
     into the panel rather than wrapping the header onto a second line. */
  .hdr__end .btn--sm { display: none; }
  /* NO JS: the nav drops to its own full-width row and wraps. Usable. */
  .hdr__nav { order: 3; width: 100%; flex-wrap: wrap; gap: .35rem 1.25rem;
              margin-left: 0; padding-top: .35rem; }
  .hdr__burger { display: inline-flex; }
  .hdr:not(.is-stuck) .hdr__nav { border-top: 1px solid var(--linea-dark); }

  /* WITH JS: 8.4 — the nav becomes a full-screen panel behind the burger. */
  .js .hdr__nav {
    position: fixed; inset: 0; z-index: 45;
    display: grid; align-content: center; justify-items: start;
    gap: 0; width: 100%; padding: 6rem var(--gutter) 3rem;
    background: rgba(5, 7, 10, .97); border-top: 0;
    opacity: 0; visibility: hidden; translate: 0 -1.25rem;
    transition: opacity .3s ease, translate .35s var(--ease), visibility .3s;
    overflow-y: auto;
  }
  .js .hdr__nav.is-open { opacity: 1; visibility: visible; translate: 0 0; }
  .js .hdr__nav a {
    width: 100%; max-width: 26rem; padding: 1rem 0;
    font-size: clamp(1.3rem, 1.05rem + 1.4vw, 1.9rem); font-weight: 300;
    letter-spacing: -.01em; text-transform: none;
    border-bottom: 1px solid var(--linea-dark);
  }
  .js .hdr__nav a::after { display: none; }
  .js .hdr__burger { position: relative; z-index: 46; }
  /* the language picker stays reachable over the open panel — it is the one
     control a visitor may want while the menu is up */
  .js .hdr__nav.is-open ~ .hdr__end { position: relative; z-index: 46; }
  /* the panel's call to action, styled as the button the header gave up */
  .js .hdr__nav .nav__cta {
    display: inline-flex; align-items: center; justify-content: center;
    width: auto; margin-top: 1.75rem; padding: .95rem 1.9rem;
    background: var(--senal); color: var(--noche); border: 0;
    font-size: .95rem; font-weight: 600; letter-spacing: .04em;
  }
}
@media (min-width: 901px) { .hdr__burger { display: none; } .hdr__nav .nav__cta { display: none; } }
@media (max-width: 560px)  { .hdr { gap: .5rem; } }
@media (max-width: 420px)  {
  /* the button keeps its accessible name; only the printed word goes */
  .hdr__burger-txt { position: absolute; width: 1px; height: 1px; overflow: hidden;
                     clip-path: inset(50%); white-space: nowrap; }
}
@media (max-width: 460px)  { .hdr__end .btn--sm { display: none; } }  /* the hero CTA is 1 screen away */
body.is-locked { overflow: hidden; }

/* ── 9 · act I — the hero ──────────────────────────────────────────────── */
.hero {
  position: relative; isolation: isolate;
  /* the bottom padding clears the «Continuar» indicator — at 1440 the buttons
     were landing on top of it in the first render */
  min-height: 100svh; padding: 7.5rem var(--gutter) 7rem;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; background: var(--noche); color: var(--tinta-dark);
  overflow: clip;
}
/* the card's own particle wave — our artwork, used exactly twice in the site */
.hero__field { position: absolute; inset: 0; z-index: -1; opacity: .55; pointer-events: none; }
.hero__field img { position: absolute; top: 50%; translate: 0 -50%;
                   height: min(88%, 620px); width: auto; max-width: none; }
.hero__field-l { left: max(-6vw, -90px); }
.hero__field-r { right: max(-6vw, -90px); }
@media (max-width: 700px) { .hero__field { opacity: .38; } }

/* ── the mark's entry ──────────────────────────────────────────────────────
   Pere, 2026-07-30: «nada más cargue la web, quiero que hagas como una
   animación con el logo, como por ejemplo dar una vuelta en 3D y luego se
   coloque de forma céntrica en la pantalla (justo arriba del slogan)».

   One turn on the Y axis, then it settles into place. Three deliberate choices:
     · it runs ONCE, on load. A mark that keeps turning stops being a mark.
     · `both` holds the last frame, so an interrupted animation can never leave
       the logo stranded edge-on — the same class of bug as the count-up that
       stranded a wrong number in v2.
     · the small delay lets the 28 KB WebP decode, so the turn is not performed
       by an empty box on a slow connection. */
/* ⚠️ `display: grid; justify-items: center` is what actually centres the mark,
   and it is not optional. Pere, 2026-07-30: «quiero el logo en la portada … en
   el centro de la imagen, no a un lado» — he was reading a real defect that
   this wrapper introduced. The hero centres its children with flex
   `align-items: center`, so the logo was centred while it was a direct child.
   Wrapping it in this div (needed for `perspective`, which must sit on the
   PARENT of the rotating element) made it a plain block box instead, and the
   global `img { display: block }` in §4 means a block image with a set width
   sits hard LEFT — `text-align: center` cannot move it. Measured before the
   fix: logo centre 575px against a 720px axis at 1440, 125 against 250 at 500. */
.hero__mark { perspective: 1000px; margin-bottom: 1.75rem;
              display: grid; justify-items: center; }
/* bigger than v3's clamp(160px, 30vw, 300px). Pere, 2026-07-30: he wants the
   mark to BE the front page, centred, not a header-sized badge above the text. */
.hero__logo { width: clamp(210px, 36vw, 400px); height: auto; }

/* ── one logo on the front page, and it is the centred one ─────────────────
   Pere: «quiero el logo en la portada … en el centro de la imagen, no a un
   lado.» On the home page the header mark sat top-left while the hero's own
   logo filled the screen — two marks, one of them off to the side. The header
   one now stands down until the bar goes solid (`is-stuck`, past 40px), so at
   rest there is exactly one logo and it is on the axis.

   It keeps its box so nothing shifts when it returns, and it comes back on
   keyboard focus so the link is never unreachable while invisible. A browser
   without `:has()` just keeps it visible — the previous behaviour, not a bug.
   The inner pages are untouched: they have no hero logo, so their header mark
   is the only one they have. */
body:has(.hero) .hdr__mark { transition: opacity .35s var(--ease); }
body:has(.hero) .hdr:not(.is-stuck) .hdr__mark { opacity: 0; pointer-events: none; }
body:has(.hero) .hdr:not(.is-stuck) .hdr__mark:focus-visible {
  opacity: 1; pointer-events: auto;
}

@media (prefers-reduced-motion: no-preference) {
  /* ⚠️ 1050ms + 100ms delay. The hero's other on-load delays in §17 are tuned
     to this landing time — change one and change the other. */
  .hero__logo { animation: logo-in 1050ms var(--ease) 100ms both; }
  @keyframes logo-in {
    from { transform: rotateY(-200deg) translateY(-16px) scale(.84); opacity: 0; }
    55%  { opacity: 1; }
    to   { transform: rotateY(0deg) translateY(0) scale(1); opacity: 1; }
  }
}

/* the slogan, set the way the business card sets it: the two nouns in cyan.
   It is brand, so it is NOT translated — it reads the same in all five. */
.slogan {
  font-family: var(--display); font-weight: 500;
  font-size: clamp(.7rem, .62rem + .38vw, .88rem);
  letter-spacing: .26em; text-transform: uppercase; color: var(--gris-dark);
  margin-bottom: 1.6rem; max-width: none;
}
.slogan span { display: block; }
.slogan em { font-style: normal; color: var(--senal); }
@media (min-width: 720px) {
  .slogan span { display: inline; }
  .slogan span + span::before { content: " "; }
}

/* ── the slogan AS THE HEADLINE ────────────────────────────────────────────
   Pere, 2026-07-30: «el slogan tiene más importancia que el texto de debajo».
   So on the home page it is the <h1> and the largest thing under the mark.

   Two things it must not inherit from the small version: the card's .26em
   tracking, which at 2.6rem stops reading as words and starts reading as rows
   of letters; and the 720px rule that runs the two halves together — the two
   lines are what makes it symmetrical, so they stay two lines at every width. */
.slogan--hero {
  font-weight: 300; color: var(--tinta-dark);
  font-size: clamp(1.3rem, .78rem + 2.15vw, 2.6rem);
  letter-spacing: .085em; line-height: 1.34;
  margin-bottom: 1.15rem; text-transform: uppercase;
}
.slogan--hero span { display: block; }
.slogan--hero span + span::before { content: none; }

/* The old hero-headline rule was removed on 2026-07-30, when the three-verb
   headline left the first screen for the creed band below. Its hard-won lesson
   moved with it: ⚠️ the middle of that clamp is set by the LONGEST language,
   not by Spanish. Measured across all five, at 1.1rem + 4.3vw the Spanish held
   its three lines from ~555 px up while Dutch and Norwegian — which cannot say
   "no se improvisa" in 24 characters — broke to four between 560 and 900 px.
   A headline tuned in one language and broken in another is tuned to one
   audience. `.creed__t` is deliberately smaller than the old hero H1 for the
   same reason. */
.hero .rule { margin-inline: auto; transform-origin: center; }

/* ── «el credo» ────────────────────────────────────────────────────────────
   The three-verb statement of method that used to be the hero's H1. It left
   the first screen on 2026-07-30 (Pere: «prefiero que no haya tanto texto…
   logo, slogan y descripción sencilla») and got its own band instead, so
   Diego's authored copy — and all four translations of it — is kept rather
   than deleted, and the first screen still lightens by three lines.

   It doubles as the transition Pere asked for in the same message: the first
   thing after the hero is one short centred statement, revealed line by line,
   which reads as a beat between two sections instead of a wall arriving. */
/* the block padding and the paper ground come from `.act--paper`; this only
   owns the inline gutter and the centring, so nothing is declared twice */
.creed { padding-inline: var(--gutter); text-align: center; }

/* ⚠️ The creed and «Qué hacemos» are BOTH paper bands, so they each brought a
   full `--bleed` to the seam — up to 16rem of empty paper between one short
   sentence and the next heading. That is precisely the defect Diego recorded
   for two adjacent night bands ("twelve rems of dead black"), reappearing on
   the other ground. The second band gives up its top bleed. */
.creed + .act--paper { padding-top: clamp(1.25rem, 4vh, 2.75rem); }
/* ⚠️ THE FLOOR OF THIS CLAMP IS A COLLISION FIX, NOT A TASTE CHOICE.
   Each child span here is one authored line inside an `.ln` clip box, and that
   box is sized for ONE visual line — so if a line wraps, the wrapped part is
   either clipped or lands on the line below. At 320px with the first floor
   (1.55rem/26ch) «Una web no se improvisa.» needed ~322px in a 280px column and
   phone-fit caught it overlapping «Se define, se construye».

   The floor is set by the LONGEST language, not by Spanish — the same rule the
   old hero headline was tuned to. Norwegian «Nettsider improviseres ikke.» is
   28 characters against the Spanish 24, so 28 characters must fit in 280px.
   Anything that raises this floor must be re-measured at 320px in Norwegian. */
.creed__t {
  font-family: var(--display); font-weight: 200;
  font-size: clamp(1.15rem, .78rem + 1.85vw, 2.5rem);
  letter-spacing: -.02em; line-height: 1.22;
  margin-inline: auto;
}
/* ⚠️ NO `ch` CAP BELOW 700px — that cap was the actual bug, not the font size.
   A 24ch box cannot hold a 28-character line at ANY font size, because `ch`
   scales with the type: Norwegian «Nettsider improviseres ikke.» is 28 against
   the Spanish 24, so it wrapped inside its own `.ln` box and landed on the line
   below it. On a phone the lines are the authored ones and the column is
   already narrow, so it needs no measure cap at all. Above 700px the cap comes
   back to stop the three lines sprawling on a wide screen. */
@media (min-width: 700px) { .creed__t { max-width: 24ch; } }
.creed__t > span, .creed__t > em { display: block; }
.creed__t em { font-style: normal; color: var(--senal-tinta); }
.hero__lead { max-width: 52ch; margin-inline: auto; color: var(--gris-dark); font-size: 1.075rem; }
.hero__acts { display: flex; flex-wrap: wrap; gap: .85rem; justify-content: center; margin-top: 2.25rem; }
.hero__scroll {
  position: absolute; bottom: 1.5rem; left: 50%; translate: -50% 0;
  font-family: var(--display); font-size: .68rem; letter-spacing: .3em;
  text-transform: uppercase; color: var(--gris-dark);
}
.hero__scroll::after { content: ""; display: block; width: 1px; height: 26px;
                       margin: .5rem auto 0; background: linear-gradient(var(--senal), transparent); }

/* ── 10 · the page header on the inner pages ───────────────────────────
   A short night band, deliberately NOT a second hero: no 100svh, no logo,
   no buttons. It exists to say which page you are on. */
/* ⚠️ CENTRED, to match the hero. Pere, 2026-07-30: «debes respetar en todas las
   páginas simetría.» The home page opens on an axis and these two opened
   left-aligned, so the site changed shape when you moved between its pages.

   The particle wave moves with it: it was hung off the RIGHT edge only, which
   is the one thing on this band that cannot be symmetrical about the same axis.
   It is now centred and mirrored, so the band reads the same from both sides. */
.phead {
  position: relative; isolation: isolate; overflow: clip;
  background: var(--noche); color: var(--tinta-dark);
  padding: clamp(8rem, 16vh, 11rem) var(--gutter) clamp(3.5rem, 8vh, 5.5rem);
  text-align: center;
}
.phead__field { position: absolute; inset: 0; z-index: -1; opacity: .3; pointer-events: none; }
.phead__field img { position: absolute; top: 50%; translate: 0 -50%;
                    height: min(115%, 560px); width: auto; max-width: none; }
.phead__field-l { left:  max(-8vw, -110px); }
.phead__field-r { right: max(-8vw, -110px); }
@media (max-width: 700px) { .phead__field { opacity: .2; } }

.crumb { font-family: var(--display); font-size: .74rem; letter-spacing: .16em;
         text-transform: uppercase; color: var(--gris-dark);
         display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1.5rem;
         justify-content: center; }
.crumb a { text-decoration: none; min-height: 44px; display: inline-flex; align-items: center; }
.crumb a:hover { color: var(--senal); }
.crumb > span:last-child { display: inline-flex; align-items: center; min-height: 44px;
                           color: var(--tinta-dark); }

/* `margin-inline: auto` is what actually centres these: `text-align` centres the
   LINES inside the box, but a box with a max-width still sits hard left without
   it — the difference between centred text and a centred column. */
.phead__h1 { font-size: clamp(2.1rem, 1.3rem + 3.6vw, 3.9rem); font-weight: 200;
             letter-spacing: -.03em; max-width: 18ch; margin-inline: auto; }
/* ⚠️ SCOPED TO `.lines` ON PURPOSE — do not widen it back.
   Two different mechanisms produce the lines in a heading, and they need
   opposite things from this rule:
     · `class="lines"`  — the lines are AUTHORED as child <span>/<em>, so each
       child must be a block. quienes-somos and nuestro-trabajo work this way.
     · `[data-lines]`   — main.js measures where the text actually wraps, and to
       do that it puts every WORD in its own direct-child <span> and reads their
       offsetTop. Those probes must stay INLINE.
   Unscoped, this rule made every word probe a block, so every word measured as
   its own line: the three new pages' h1 came back from a language switch as a
   column of single words with no spaces between them. The rebuilt lines are
   still blocks — `.ln` sets that itself in §17 — so nothing else changes. */
.phead__h1.lines > span, .phead__h1.lines > em { display: block; }
.phead__h1 em { font-style: normal; color: var(--senal); font-weight: 300; }
.phead__lead { max-width: 58ch; margin-top: 1.5rem; color: var(--gris-dark); font-size: 1.075rem;
               margin-inline: auto; }
/* The eyebrow needs BOTH, and the second one is the easy miss: it is a <p>, so
   the global `:where(p, dd) { max-width: 62ch }` caps its box at ~562px. Inside
   that box `justify-content` did centre the text — but the box itself still sat
   hard left, so the label rendered a quarter of the way across a centred band.
   `margin-inline: auto` centres the box; `justify-content` centres the row in
   it. Measured at 1440: mid 377px → 720px. */
.phead .eyebrow { justify-content: center; margin-inline: auto; }
.phead .rule { margin-inline: auto; transform-origin: center; }

/* ── 11 · act II — paper ───────────────────────────────────────────────── */
.act--paper { background: var(--papel); padding-block: var(--bleed); }
.act--tight { padding-top: 0; }
.act--paper > .wrap > .h2 { margin-bottom: 2.75rem; }

/* 9.3 sticky figure narrative — the heading holds while the beats scroll past */
.narrative { display: grid; gap: clamp(2rem, 5vw, 4.5rem); }
.narrative__beats { display: grid; gap: clamp(2.75rem, 6vh, 4.5rem); }
.beat__n { font-family: var(--display); font-size: .78rem; font-weight: 600;
           letter-spacing: .2em; color: var(--senal-tinta); margin-bottom: .55rem; }
.beat h3 { font-size: clamp(1.2rem, 1.05rem + .55vw, 1.5rem); font-weight: 400;
           margin-bottom: .6rem; }
.beat p { color: var(--gris); font-size: 1.0625rem; }
.beat + .beat { padding-top: clamp(2.75rem, 6vh, 4.5rem); border-top: 1px solid var(--linea); }

@media (min-width: 900px) {
  .narrative { grid-template-columns: minmax(0, 22rem) minmax(0, 1fr); }
}

/* the numbered argument list — «por qué una web», «principios», «el sistema» */
.why { list-style: none; padding: 0; margin: 0; display: grid; gap: 0;
       border-top: 1px solid var(--linea); }
.why__i { padding: 1.75rem 0; border-bottom: 1px solid var(--linea);
          display: grid; gap: .35rem 2rem; }
.why__n { font-family: var(--display); font-size: .78rem; font-weight: 600;
          letter-spacing: .2em; color: var(--senal-tinta); }
.why__i h3 { font-size: clamp(1.15rem, 1.05rem + .5vw, 1.45rem); font-weight: 500; }
.why__i p { color: var(--gris); }
@media (min-width: 820px) {
  .why__i { grid-template-columns: 4rem minmax(0, 14rem) minmax(0, 1fr); align-items: baseline; }
  .why__n { grid-row: 1; }
}

/* the cascade — cards offset 0 / 52 / 104px, arriving .18s apart */
.cards { display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); }
.card { background: var(--papel-2); border: 1px solid var(--linea); padding: clamp(1.5rem, 3vw, 2.25rem);
        transition: transform .45s var(--ease), box-shadow .45s ease, border-color .45s ease; }
.card:hover { transform: translateY(-8px); box-shadow: 0 22px 55px rgba(14, 22, 32, .1);
              border-color: #C6D2DB; }
.card__n { display: block; font-family: var(--display); font-size: 2.4rem; font-weight: 200;
           color: var(--senal-tinta); opacity: .38; line-height: 1; margin-bottom: .9rem; }
.card h3 { font-size: 1.22rem; font-weight: 500; margin-bottom: .55rem; }
.card p { color: var(--gris); font-size: 1rem; }
@media (min-width: 900px) {
  .card:nth-child(2) { margin-top: 52px; transition-delay: .18s; }
  .card:nth-child(3) { margin-top: 104px; transition-delay: .36s; }
}

/* the five phases — a real <ol>, readable with no CSS and no JS */
.steps { list-style: none; padding: 0; counter-reset: s;
         display: grid; gap: 0; border-top: 1px solid var(--linea); }
.step { counter-increment: s; display: grid; gap: .3rem 1.75rem;
        padding: 1.6rem 0; border-bottom: 1px solid var(--linea); }
.step::before {
  content: counter(s, decimal-leading-zero);
  font-family: var(--display); font-size: .95rem; font-weight: 600;
  letter-spacing: .1em; color: var(--senal-tinta);
}
.step h3 { font-size: 1.2rem; font-weight: 500; }
.step p { color: var(--gris); font-size: 1rem; }
.step p b { font-weight: 600; color: var(--tinta); }
@media (min-width: 760px) {
  /* the phase name gets the width the wider measure freed up, instead of the
     row ending in 300px of empty paper: 15rem → 19rem, and the description
     column keeps its own 62ch cap, so no line got longer. */
  .step { grid-template-columns: 4.5rem minmax(0, 19rem) minmax(0, 1fr); align-items: baseline; }
  .step::before { grid-row: 1; }
}

/* the contract terms. ⚠️ NO € FIGURE MAY EVER APPEAR HERE — Brief §7. */
.terms { margin-top: 2.75rem; padding: clamp(1.35rem, 3vw, 2rem);
         background: var(--papel-2); border: 1px solid var(--linea);
         border-left: 2px solid var(--senal-tinta); }
.terms__h { font-family: var(--display); font-size: .72rem; font-weight: 600;
            letter-spacing: .22em; text-transform: uppercase; color: var(--senal-tinta);
            margin-bottom: 1rem; }
/* Two columns from 900px up. With the measure at 1280 this panel is ~1200px
   wide while every line inside it stops at 62ch — which left the whole right
   half of the box empty, the exact "second column is empty on desktop" defect
   Diego recorded. Two columns fill the box without lengthening a single line,
   so the width is used and readability is untouched. */
.terms__l { list-style: none; padding: 0; margin: 0; display: grid; gap: .7rem; }
@media (min-width: 900px) {
  .terms__l { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .8rem 3rem; }
}
.terms__l li { position: relative; padding-left: 1.5rem; color: var(--gris); font-size: 1rem;
               max-width: 62ch; }
.terms__l li::before { content: ""; position: absolute; left: 0; top: .65em;
                       width: 8px; height: 1px; background: var(--senal-tinta); }
.terms__l b { font-weight: 600; color: var(--tinta); }

/* the delivery spec — «qué entrega un proyecto» */
.spec { list-style: none; padding: 0; margin: 0; display: grid; gap: 1.25rem;
        grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr)); }
.spec__i { border-top: 2px solid var(--senal-tinta); padding-top: 1.1rem; }
.spec__i h3 { font-size: 1.12rem; font-weight: 500; margin-bottom: .4rem; }
.spec__i p { color: var(--gris); font-size: .98rem; }

/* the figures. Every one is counted on disk, never estimated (Brief §6, §12). */
.figs { display: grid; gap: 1.25rem; margin: 0 0 3rem;
        grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr)); }
.fig { border-top: 2px solid var(--senal-tinta); padding-top: 1rem; }
.fig__n { font-family: var(--display); font-size: clamp(2.6rem, 2rem + 2.4vw, 4rem);
          font-weight: 200; line-height: 1; font-variant-numeric: lining-nums tabular-nums; }
.fig dd { color: var(--gris); font-size: .95rem; margin-top: .35rem; }
.figs--dark { margin: 3rem 0 0; }
.figs--dark .fig { border-top-color: var(--senal); }
.figs--dark .fig__n { color: var(--tinta-dark); }
.figs--dark .fig dd { color: var(--gris-dark); }
.sectors { margin-top: 0; }

/* the reserved spot — an honest empty shelf, labelled */
.reserved__slot {
  display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 1.5rem;
  margin-top: 2.75rem; padding: clamp(1.5rem, 4vw, 2.5rem);
  border: 1px dashed #C2CDD6; background: repeating-linear-gradient(
    -45deg, transparent 0 12px, rgba(10, 110, 138, .035) 12px 24px);
}
.reserved__tag {
  font-family: var(--display); font-size: .7rem; font-weight: 600; letter-spacing: .22em;
  text-transform: uppercase; color: var(--senal-tinta);
  border: 1px solid var(--senal-tinta); padding: .35rem .7rem;
}
.reserved__cta { font-family: var(--display); font-size: 1.05rem; color: var(--tinta);
                 text-decoration: none; border-bottom: 1px solid var(--senal-tinta);
                 padding-bottom: 2px; min-height: 44px; display: inline-flex; align-items: center; }
.reserved__cta:hover { color: var(--senal-tinta); }

/* ══════════════════════════════════════════════════════════════════════════
   11b · SIMETRÍA — every band built on the page's centre line
   Pere, 2026-07-30: «sobre todo quiero simetría, que todo se vea bien ordenado
   y visual», after «debes respetar en todas las páginas simetría» and
   «quiero todo más visual y ordenado».

   Everything above this block was built asymmetric on purpose: the heading on
   the left with the content on the right (`.head--split`, `.narrative`), a
   sticky aside holding one column while the other scrolled, and three cards
   stepped down a 0 / 52 / 104px staircase. That was the v2 design. This
   overrides it rather than deleting it, so it is one block to revert if the two
   of you disagree — and the staircase and the sticky aside are the two things
   Diego will notice are gone.

   ⚠️ ONE DELIBERATE EXCEPTION. Multi-line body paragraphs stay LEFT-aligned
   inside their card. Centred prose at 60+ characters makes the eye hunt for
   where each line begins, and «ordenado» was asked for in the same sentence as
   «simétrico». So the STRUCTURE is symmetrical — heads, numbers, titles, and
   every grid an even count — and the reading is not sacrificed to it. Card
   titles and numbers ARE centred, because they are one line and they are what
   carries the sense of order.
   ══════════════════════════════════════════════════════════════════════════ */

/* 1 · every section head on the axis, paper and night alike */
.act .eyebrow { justify-content: center; margin-inline: auto; }
.act .rule    { margin-inline: auto; transform-origin: center; }
.act .h2, .act .lead--p { margin-inline: auto; text-align: center; }

/* the two-column head collapses to one centred column */
.head--split { justify-items: center; text-align: center; }
.head--split > div { display: grid; justify-items: center; }
@media (min-width: 900px) {
  .head--split { grid-template-columns: minmax(0, 1fr); }
  .head--split > p { padding-bottom: 0; }
}

/* 2 · «Qué hacemos» — centred head, then the four beats as an even grid.
   The sticky aside goes static: sticking only means anything when there is a
   second column beside it to scroll past, and there no longer is. */
.narrative { justify-items: center; }
@media (min-width: 900px) {
  .narrative { grid-template-columns: minmax(0, 1fr); }
}
.narrative__beats { width: 100%; }
@media (min-width: 760px) {
  .narrative__beats { grid-template-columns: repeat(2, minmax(0, 1fr));
                      gap: clamp(1.25rem, 2.5vw, 2rem); }
}
/* a stacked list separates with a rule between items; a grid has no "previous
   item", so the divider becomes the card's own edge */
.beat { background: var(--papel-2); border: 1px solid var(--linea);
        padding: clamp(1.35rem, 3vw, 2rem); }
.beat + .beat { padding-top: clamp(1.35rem, 3vw, 2rem); border-top: 1px solid var(--linea); }
.beat__n, .beat h3 { text-align: center; }

/* 3 · «Por qué una web» — four items, so an even 2×2 */
.why { border-top: 0; gap: clamp(1.25rem, 2.5vw, 1.5rem); }
@media (min-width: 760px) { .why { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.why__i { border-bottom: 0; background: var(--papel-2); border: 1px solid var(--linea);
          padding: clamp(1.35rem, 3vw, 1.9rem); }
.why__n, .why__i h3 { text-align: center; }
/* the 820px three-column row would fight the 2×2 — one item per cell now */
@media (min-width: 820px) {
  .why__i { grid-template-columns: minmax(0, 1fr); align-items: start; }
}

/* 4 · «Qué incluye» — the staircase is levelled. Three cards at three
   different heights is the most visible asymmetry on the page. */
@media (min-width: 900px) {
  .card:nth-child(2), .card:nth-child(3) { margin-top: 0; }
}
.card h3, .card .card__n { text-align: center; }

/* 5 · the contract panel: its heading on the axis too.
   `margin-inline: auto` again — it is a <p>, so §4's `max-width: 62ch` caps its
   box and `text-align` alone left it 336px off the axis at 1440. That is the
   third element this has caught in one round; the rule of thumb is that any <p>
   being centred in this codebase needs the margin as well as the alignment. */
.terms__h { text-align: center; margin-inline: auto; }

/* ══════════════════════════════════════════════════════════════════════════
   11c · «LAS PÁGINAS» — the way in from the front page
   Pere, 2026-07-30: every top-nav item became its own page, so the home page
   opens onto them instead of holding all their text.

   Four cards, so a 2×2 on desktop — the same even grid as «Qué hacemos» and
   «Por qué una web», for the symmetry Pere asked for. The whole card is the
   link: a 2-line target is a far easier tap than the heading alone, and it
   keeps one focus stop per destination instead of two.
   ══════════════════════════════════════════════════════════════════════════ */
.ways { list-style: none; padding: 0; margin: 0; display: grid;
        gap: clamp(1.25rem, 2.5vw, 1.5rem); }
@media (min-width: 760px) { .ways { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.way a {
  display: grid; gap: .5rem; height: 100%;
  padding: clamp(1.5rem, 3vw, 2.15rem);
  background: var(--papel-2); border: 1px solid var(--linea);
  text-decoration: none; color: inherit; text-align: center;
  transition: border-color .3s var(--ease), transform .3s var(--ease),
              box-shadow .3s var(--ease);
}
.way a:hover, .way a:focus-visible {
  border-color: var(--senal-tinta); transform: translateY(-4px);
  box-shadow: 0 18px 44px rgba(14, 22, 32, .10);
}
.way__n { font-family: var(--display); font-size: .78rem; font-weight: 600;
          letter-spacing: .2em; color: var(--senal-tinta); }
.way__t { font-family: var(--display); font-size: clamp(1.25rem, 1.05rem + .7vw, 1.6rem);
          font-weight: 400; line-height: 1.15; }
/* the destination's own heading, so the card says where it actually goes */
.way__p { color: var(--gris); font-size: 1rem; max-width: 34ch; margin-inline: auto; }
/* the arrow is the only ornament, and it is a glyph rather than an image */
.way__t::after { content: " →"; color: var(--senal-tinta); }

/* ── 12 · el órgano administrativo ─────────────────────────────────────
   A real <figure> with a real <figcaption>. Every connector is CSS, so with
   no stylesheet at all this is still a heading, two people and four
   functions, in that order. */
.org { margin: 0; }
.org__head { display: flex; justify-content: center; }
.org__brand {
  font-family: var(--display); font-size: .78rem; font-weight: 600; letter-spacing: .3em;
  color: var(--senal-tinta); border: 1px solid var(--senal-tinta); padding: .6rem 1.4rem;
}
/* the trunk, the crossbar, then a riser into each partner — all drawn in CSS,
   so with no stylesheet this is still a heading and a two-item list */
.org__row {
  list-style: none; padding: 0; margin: 0; position: relative;
  display: grid; gap: 1.5rem; padding-top: 4rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  max-width: 46rem; margin-inline: auto;
}
.org__row::before {                       /* trunk out of the brand box */
  content: ""; position: absolute; top: 0; left: 50%; width: 1px; height: 1.9rem;
  background: var(--linea); translate: -.5px 0;
}
.org__row::after {                        /* the crossbar over both partners */
  content: ""; position: absolute; top: 1.9rem; left: 25%; right: 25%; height: 1px;
  background: var(--linea);
}
.org__person::before {                    /* the riser down into each card */
  content: ""; position: absolute; bottom: 100%; left: 50%; width: 1px; height: 2.1rem;
  background: var(--linea); translate: -.5px 0;
}
@media (max-width: 600px) {               /* stacked: one straight spine, no crossbar */
  .org__row::after { display: none; }
  .org__person::before { height: 1.5rem; }
  .org__row { padding-top: 3.4rem; }
}
.org__person {
  position: relative; border: 1px solid var(--linea); background: var(--papel-2);
  padding: clamp(1.35rem, 3vw, 2rem);
  transition: border-color .4s ease, transform .4s var(--ease);
}
.org__person:hover { border-color: #C6D2DB; transform: translateY(-4px); }
.org__mono {
  display: inline-flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; margin-bottom: 1rem;
  border: 1px solid var(--senal-tinta); color: var(--senal-tinta);
  font-family: var(--display); font-size: .95rem; font-weight: 600; letter-spacing: .06em;
}
.org__name { font-size: 1.28rem; font-weight: 400; }
.org__role { margin-top: .25rem; font-family: var(--display); font-size: .72rem; font-weight: 600;
             letter-spacing: .2em; text-transform: uppercase; color: var(--gris); }
.org__line { margin-top: .9rem; padding-top: .9rem; border-top: 1px solid var(--linea);
             color: var(--gris); font-size: .95rem; }

.org__cap { margin: 3rem 0 1.5rem; display: grid; gap: .35rem; }
.org__caph { font-family: var(--display); font-size: .72rem; font-weight: 600;
             letter-spacing: .22em; text-transform: uppercase; color: var(--senal-tinta); }
.org__cap span:last-child { color: var(--gris); font-size: .95rem; }

.org__fns { list-style: none; padding: 0; margin: 0; display: grid; gap: 0;
            border-top: 1px solid var(--linea); }
.org__fn { padding: 1.4rem 0; border-bottom: 1px solid var(--linea); display: grid; gap: .3rem; }
.org__fn h4 { font-family: var(--display); font-size: 1.05rem; font-weight: 500; }
.org__fn p { color: var(--gris); font-size: .98rem; }
@media (min-width: 760px) {
  .org__fn { grid-template-columns: minmax(0, 15rem) minmax(0, 1fr); gap: 0 2rem; align-items: baseline; }
}

/* ── 13 · act III — night ──────────────────────────────────────────────── */
.act--night { position: relative; isolation: isolate; overflow: clip;
              background: var(--noche); color: var(--tinta-dark); padding-block: var(--bleed); }
/* Three night bands run back to back on the home page. Left alone, each one
   closes with a full bleed and the next opens with another, so the seam came
   out roughly twelve rems of empty black — visible as dead space between «el
   sistema» and «dónde» in the 1440px render. The pair below collapses the two
   paddings into one deliberate gap. Where :has() is unsupported the seam is
   merely generous, never broken. */
.act--night + .act--night { padding-top: clamp(3rem, 7vh, 5rem); }
.act--night:has(+ .act--night) { padding-bottom: 0; }

/* «nuestro sistema» — the differentiator, on our own ground.
   The reading here is short and structured, which is why it is allowed to sit
   on the night: the rule that matters is that CYAN never carries body text on
   paper, and that continuous prose never runs long on black. */
/* this heading is one long sentence and the default 20ch measure broke it
   into five lines against a three-line paragraph — the columns stopped
   reading as a pair. Wider and a step smaller, it lands in four. */
.sys .h2 { max-width: 26ch; font-size: clamp(1.8rem, 1.3rem + 2vw, 2.8rem); }
.sys__lead { max-width: 60ch; color: var(--gris-dark); font-size: 1.0625rem; }
.sys__grid { list-style: none; padding: 0; margin: 0; display: grid; gap: 0;
             border-top: 1px solid var(--linea-dark); }
.sys__i { padding: 1.75rem 0; border-bottom: 1px solid var(--linea-dark);
          display: grid; gap: .35rem 2rem; }
.sys__n { font-family: var(--display); font-size: .78rem; font-weight: 600;
          letter-spacing: .2em; color: var(--senal); }
.sys__i h3 { font-size: clamp(1.15rem, 1.05rem + .5vw, 1.45rem); font-weight: 500;
             color: var(--tinta-dark); }
.sys__i p { color: var(--gris-dark); }
@media (min-width: 820px) {
  .sys__i { grid-template-columns: 4rem minmax(0, 14rem) minmax(0, 1fr); align-items: baseline; }
  .sys__n { grid-row: 1; }
}
.sys__note { margin-top: 2.5rem; padding-left: 1rem; border-left: 2px solid var(--senal);
             color: var(--gris-dark); font-size: .92rem; max-width: 62ch; }

/* «el circuito» — the four gates, drawn. The connecting line is one rule
   behind the row; each node carries the section's own ground as a solid fill,
   which is what makes the line read as a connector rather than a strike
   through. Night-only by construction: the fill is --noche. */
.circuit {
  list-style: none; margin: 2.75rem 0 0; padding: 0; position: relative;
  display: flex; align-items: center; justify-content: space-between; gap: .4rem;
}
.circuit::before { content: ""; position: absolute; left: 0; right: 0; top: 50%;
                   height: 1px; background: var(--linea-dark); }
.circuit__i {
  position: relative; background: var(--noche);
  display: flex; align-items: center; gap: .5rem;
  padding: .55rem .85rem; border: 1px solid var(--linea-dark);
  font-family: var(--display); font-size: .78rem; font-weight: 500;
  letter-spacing: .05em; color: var(--tinta-dark); white-space: nowrap;
}
.circuit__i b { color: var(--senal); font-weight: 600; font-size: .68rem; letter-spacing: .12em; }
.circuit__i--edge { border-style: dashed; color: var(--gris-dark); font-weight: 400; }
.circuit__i--out  { border-style: solid; border-color: var(--senal); color: var(--senal); }
.circuit__note { margin-top: 1.1rem; font-size: .82rem; color: var(--gris-dark); max-width: 62ch; }

/* stacked: the row's single rule would cut through wrapped items, so each node
   grows its own tick down to the next one instead */
@media (max-width: 940px) {
  .circuit { flex-direction: column; align-items: stretch; gap: 1.1rem; }
  .circuit::before { display: none; }
  .circuit__i { justify-content: flex-start; white-space: normal; }
  .circuit__i:not(:last-child)::after {
    content: ""; position: absolute; left: 1.4rem; top: 100%; width: 1px; height: 1.1rem;
    background: var(--linea-dark);
  }
}

/* the closing call to action on the inner pages */
.cta__in { display: grid; gap: 2rem 4rem; }
.cta__acts { display: grid; gap: 1.5rem; justify-items: start; align-content: end; }
.cta__acts p { color: var(--gris-dark); max-width: 46ch; }
@media (min-width: 900px) {
  .cta__in { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: end; }
}

/* ── 14 · dónde — the map section ──────────────────────────────────────
   The map is a flat image baked from OpenStreetMap tiles at build time. It
   calls nobody on load; the Google Maps button is a plain link. */
.donde__p { max-width: 58ch; color: var(--gris-dark); }

.map { margin: 2.75rem 0 0; }
.map__frame {
  position: relative; overflow: clip;
  border: 1px solid var(--linea-dark); background: var(--noche-2);
  aspect-ratio: 1196 / 734;
}
.map__img { width: 100%; height: 100%; object-fit: cover; }

/* ⚠️ The pins carry NO text. The OSM tiles already have every town's name
   printed on them, so a second label sat directly on top of the first —
   "Polop / la Nucia / La Nucía" stacked into an unreadable smear, and
   "Villajoyosa" landed on the tiles' own "la Vila Joiosa / Villajoyosa".
   Caught in the 1440px render. The dot says "this one is ours"; the map
   itself says which town it is. */
/* ⚠️ The pin layer is sized to the MAP IMAGE, not to the frame, and that is
   load-bearing. Below 560 px the plate stops being absolute and flows underneath
   the picture *inside* the frame, so the frame grows about twice as tall as the
   map — and pins positioned against `inset: 0` then land on the caption panel
   instead of on their towns. Giving the layer the image's own aspect ratio and
   letting its bottom edge float pins it to the picture at every width, because
   above 560 px the frame is exactly that ratio anyway. */
.map__pins { list-style: none; margin: 0; padding: 0; position: absolute;
             inset: 0 0 auto 0; aspect-ratio: 1196 / 734; }
.pin { position: absolute; left: var(--x); top: var(--y); translate: -50% -50%;
       width: 8px; height: 8px; background: var(--senal); border-radius: 50%;
       box-shadow: 0 0 0 3px rgba(0, 217, 255, .16), 0 0 10px var(--senal-glow); }
.pin span {                                  /* the name stays, for screen readers */
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
.pin--key { width: 12px; height: 12px;
            box-shadow: 0 0 0 4px rgba(0, 217, 255, .2), 0 0 18px var(--senal-glow); }
@media (max-width: 700px) { .pin { width: 7px; height: 7px; }
                            .pin--key { width: 10px; height: 10px; } }

/* the plate sits bottom-RIGHT, over open sea — bottom-left covered Villajoyosa */
.map__plate {
  position: absolute; right: 0; bottom: 0; max-width: min(100%, 22rem);
  padding: clamp(1rem, 3vw, 1.5rem);
  background: linear-gradient(to top left, rgba(5,7,10,.95), rgba(5,7,10,.74));
  border-top: 1px solid var(--linea-dark); border-left: 1px solid var(--linea-dark);
}
@media (max-width: 560px) { .map__plate { position: static; max-width: none; border-left: 0; } }
.map__title { font-family: var(--display); font-size: 1.05rem; font-weight: 500;
              color: var(--tinta-dark); }
.map__sub { font-size: .85rem; color: var(--gris-dark); margin: .2rem 0 .9rem; }

.map__note { margin-top: 1rem; display: grid; gap: .3rem;
             font-size: .8rem; color: var(--gris-dark); max-width: 62ch; }
.map__credit { opacity: .75; }

/* ── 15 · «el brief» ───────────────────────────────────────────────────── */
/* ── «Cuéntenos su proyecto», centred ──────────────────────────────────────
   Pere, 2026-07-30: «en la parte de cuéntenos su proyecto hazlo más céntrico,
   se ve un lateral demasiado vacío.» The form is capped at 46rem (736px) inside
   a 1248px band and was hard left, so ~510px of the band was empty — the widest
   dead margin left on the site. The form block is now centred, the wave is
   mirrored instead of hung off one edge, and the questions sit on the axis.

   ⚠️ The INPUTS and their floating labels stay left-aligned. Centred text
   inside a text field fights the caret and makes a half-typed answer jump
   around as it grows; centring a form's *fields* is the one place where
   symmetry costs usability outright. The block is centred, the writing is not. */
.brief__field { position: absolute; inset: 0; z-index: -1; opacity: .4; pointer-events: none; }
.brief__field img { position: absolute; top: 50%; translate: 0 -50%;
                    height: min(90%, 640px); width: auto; max-width: none; }
.brief__field-l { left:  max(-8vw, -120px); }
.brief__field-r { right: max(-8vw, -120px); }
@media (max-width: 700px) { .brief__field { opacity: .25; } }

.brief__lead { max-width: 56ch; margin-top: 1.25rem; color: var(--gris-dark);
               margin-inline: auto; text-align: center; }
.q { margin-top: 2.75rem; max-width: 46rem; margin-inline: auto; }

.q__set { border: 0; padding: 0; margin: 0 0 2.25rem; }
/* a <legend> is not a <p>, so it has no 62ch cap to fight — text-align is
   enough to put the question itself on the axis */
.q__legend { font-family: var(--display); font-size: 1.12rem; font-weight: 400;
             color: var(--tinta-dark); padding: 0; margin-bottom: 1rem;
             text-align: center; width: 100%; }
.q__hint { font-size: .85rem; color: var(--gris-dark); margin: -.6rem 0 .9rem;
           text-align: center; margin-inline: auto; }

.chips { display: flex; flex-wrap: wrap; gap: .6rem; justify-content: center; }
.chip { position: relative; }
.chip input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.chip span {
  display: flex; align-items: center; min-height: 46px; padding: .55rem 1.1rem;
  border: 1px solid var(--linea-dark); color: var(--gris-dark);
  font-size: .95rem; transition: color .2s, border-color .2s, background .2s;
}
.chip input:hover + span { border-color: #47525E; color: var(--tinta-dark); }
.chip input:checked + span { border-color: var(--senal); color: var(--senal);
                             background: rgba(0, 217, 255, .07); }
.chip input:focus-visible + span { outline: 2px solid var(--senal); outline-offset: 3px; }

/* 12.2 floating labels */
.fields { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); }
.field { position: relative; display: block; }
.field input {
  width: 100%; min-height: 52px; padding: 1.35rem .95rem .5rem;
  background: var(--noche-2); border: 1px solid var(--linea-dark); color: var(--tinta-dark);
  transition: border-color .2s;
}
.field input:hover { border-color: #47525E; }
.field input:focus { border-color: var(--senal); outline: none; }
.field__lbl {
  position: absolute; left: .95rem; top: 50%; translate: 0 -50%;
  color: var(--gris-dark); font-size: .95rem; pointer-events: none;
  transition: top .18s ease, font-size .18s ease, color .18s ease;
}
.field input:focus + .field__lbl,
.field input:not(:placeholder-shown) + .field__lbl {
  top: .95rem; font-size: .7rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--senal);
}

/* the composed message — always visible before anything is sent */
.q__out { margin: 2.5rem 0 1.5rem; }
.q__out-h { text-align: center;
            font-family: var(--display); font-size: .75rem; font-weight: 600;
            letter-spacing: .2em; text-transform: uppercase; color: var(--senal);
            margin-bottom: .75rem; }
.q__msg {
  font-family: var(--body); font-size: .95rem; line-height: 1.7; color: var(--tinta-dark);
  white-space: pre-wrap; word-break: break-word; margin: 0;
  padding: 1.25rem 1.35rem; min-height: 6.5rem;
  background: var(--noche-2); border: 1px solid var(--linea-dark);
  border-left: 2px solid var(--senal);
}
.q__msg:empty::before { content: attr(data-empty); color: var(--gris-dark); }

.q__send { display: flex; flex-wrap: wrap; gap: .85rem; justify-content: center; }
/* centred with the rest of the form block; the ragged edge of a left-aligned
   note under centred buttons was the last thing pulling this section left */
.q__privacy { text-align: center; margin-inline: auto; }
.q__privacy, .q__nojs { margin-top: 1.5rem; font-size: .875rem; color: var(--gris-dark);
                        max-width: 56ch; }
.q__nojs { border-left: 2px solid var(--senal); padding-left: 1rem; }

/* stepper mode — only added by JS. Without it every question is simply visible. */
.q.is-stepped .q__set { display: none; }
.q.is-stepped .q__set.is-on { display: block; animation: qin .32s ease both; }
@keyframes qin { from { opacity: 0; translate: 0 8px; } to { opacity: 1; translate: 0 0; } }
.q__bar { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.75rem; }
.q__track { flex: 1; height: 2px; background: var(--linea-dark); }
.q__fill { display: block; height: 100%; background: var(--senal); width: 0;
           transition: width .35s var(--ease); }
.q__count { font-family: var(--display); font-size: .72rem; font-weight: 600;
            letter-spacing: .16em; text-transform: uppercase; color: var(--gris-dark);
            white-space: nowrap; font-variant-numeric: lining-nums tabular-nums; }
.q__nav { display: flex; gap: .75rem; margin-top: 1.75rem; justify-content: center; }
.q__nav .btn[hidden] { display: none; }

/* ── 16 · footer — the back of the card ────────────────────────────────── */
.foot { background: var(--noche); color: var(--gris-dark); padding-block: var(--bleed) 2.5rem;
        border-top: 1px solid var(--linea-dark); }
.foot__in { display: grid; gap: 2.5rem;
            grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); }
/* The wordmark is set as TYPE, not shipped as a raster.
   Until 2026-07-30 it was `wordmark.webp`, cut out of the card render by
   `Inputs/extract-brand-assets.py`. When the studio was renamed that file still
   read the old name, and no alt text fixes a picture of the wrong word. The
   card sets its own wordmark in type too (Business-Card/front.html: PD in cyan,
   the rest in white, wide-tracked caps) — so this is the mark's own spec, not
   type imitating artwork. The monogram stays artwork; only the lettering moved.
   It now scales, translates, costs zero bytes, and can never fall out of date
   with the name again. */
.foot__wm {
  font-family: var(--display); font-weight: 600;
  font-size: clamp(1.3rem, .95rem + 1.5vw, 1.95rem);
  letter-spacing: .2em; text-transform: uppercase;
  color: var(--tinta-dark); max-width: none;
}
.foot__wm span { color: var(--senal); }
.foot__tag { margin-top: 1rem; font-family: var(--display); font-size: .78rem;
             letter-spacing: .1em; text-transform: uppercase; color: var(--gris-dark);
             max-width: 26ch; }
.foot__founders { margin-top: 1.25rem; display: grid; gap: .15rem; font-size: .82rem; }
.foot__founders span { font-family: var(--display); font-size: .7rem; font-weight: 600;
                       letter-spacing: .22em; text-transform: uppercase; color: var(--senal); }
.foot__founders b { font-weight: 400; color: var(--tinta-dark); }

/* the section index the header gave up. A long list belongs here, where length
   costs nothing, rather than across the top of every page. */
.foot__nav { display: grid; gap: .1rem; align-content: start; }
.foot__dt, .foot__block dt { font-family: var(--display); font-size: .7rem; font-weight: 600;
                  letter-spacing: .22em; text-transform: uppercase; color: var(--senal);
                  margin-bottom: .6rem; }
.foot__nav a { text-decoration: none; color: var(--tinta-dark); font-size: .93rem;
               min-height: 44px; display: inline-flex; align-items: center;
               border-bottom: 1px solid transparent; width: fit-content; }
.foot__nav a:hover { color: var(--senal); border-bottom-color: var(--senal); }
.foot__block dt ~ dt { margin-top: 1.4rem; }
.foot__block dd { margin: 0 0 .3rem; color: var(--tinta-dark); font-size: .97rem;
                  display: flex; flex-wrap: wrap; gap: .1rem .6rem; }
.foot__block dd a { text-decoration: none; border-bottom: 1px solid transparent;
                    min-height: 44px; display: inline-flex; align-items: center; }
.foot__block dd a:hover { color: var(--senal); border-bottom-color: var(--senal); }
.foot__block dd span { color: var(--gris-dark); min-width: 8.5rem; }
.foot__block dd b { font-weight: 500; font-variant-numeric: lining-nums tabular-nums; }
.closed { color: var(--senal); }

.foot__legal { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--linea-dark);
               display: grid; gap: .6rem; }
.foot__legal p { font-size: .82rem; max-width: 70ch; }
/* the link gets its own row rather than sitting inline: a 44px tap box inside a
   wrapping sentence overlapped the line above it at 320px (phone-fit, 2026-07-30) */
.foot__credit { display: grid; justify-items: start; gap: 0; }
.foot__credit a { color: var(--gris-dark); text-decoration: underline; text-underline-offset: 3px;
                  display: inline-flex; align-items: center; min-height: 44px; }
.foot__credit a:hover { color: var(--senal); }

/* ══════════════════════════════════════════════════════════════════════════
   16b · WHAT ANSWERS THE POINTER
   Pere, 2026-07-30: «donde veas que se pueda hacer la web más interactiva
   hazlo» — with the standing constraint that the page stays serious. So none
   of this moves anything, changes a colour scheme or plays a sound. Every rule
   below answers ONE question: which row am I on?

   All of it is `:hover` AND `:focus-within`, so it exists for a keyboard too —
   a hover-only affordance is a mouse-only affordance. And all of it is CSS with
   a transition, so §18's reduced-motion block flattens the movement while
   leaving the colour change, which is the part that actually informs.
   ══════════════════════════════════════════════════════════════════════════ */
.step { transition: background-color .18s var(--ease); }
.step:hover, .step:focus-within { background: var(--papel-2); }
/* the leading-zero counter is a ::before, so it recolours with the row */
.step:hover::before, .step:focus-within::before { color: var(--senal-tinta); }

.beat { transition: opacity .2s var(--ease); }
/* on a pointer device the beats you are not reading step back very slightly.
   `:has()` keeps it off any browser that cannot scope it, and the whole thing
   is skipped on touch, where there is no hover and nothing to dim. */
@media (hover: hover) and (min-width: 900px) {
  .narrative__beats:has(.beat:hover) .beat:not(:hover) { opacity: .55; }
}

/* the map pins: they are decorative and aria-hidden, so they take no label and
   no tab stop — they only acknowledge the pointer passing over the map.
   ⚠️ The class is `.pin`, not `.map__pin`; the `.map__pins` list is the parent.
   Scaling is scoped to the pin's own marker so the town label does not grow
   with it, and it is `scale`, not `transform`, because `.pin` already uses
   `transform` for its --x/--y placement — overwriting that would fling every
   pin to the corner of the map. */
.pin { transition: scale .2s var(--ease); }
.map__frame:hover .pin { scale: 1.15; }

/* ══════════════════════════════════════════════════════════════════════════
   17 · THE MOTION
   Everything below is a reveal. It plays once and stops. The page is complete
   and correct without any of it, and §18 switches all of it off.
   ══════════════════════════════════════════════════════════════════════════ */

/* — 6.1 split-line reveal —————————————————————————————————————————————
   main.js wraps each visual line in <span class="ln"><i>…</i></span>. The
   outer span clips; the inner one slides up out of it. With JS off there are
   no wrappers at all and the heading is simply a heading. */
/* the padding/negative-margin pair is not decoration: `overflow: hidden` on a
   line box set at line-height 1.1 cuts the descenders off every g, j, p, q and
   y. The padding gives the clip box room; the margin takes the space back so
   the heading's rhythm is unchanged. */
.ln { display: block; overflow: hidden; padding-bottom: .14em; margin-bottom: -.14em; }
.ln > i { display: block; font-style: inherit; }

@media (prefers-reduced-motion: no-preference) {
  /* the hero and the page headers are above the fold, so a scroll timeline
     would never fire on them — they animate once on load instead */
  .js .phead__h1 .ln > i {
    animation: lineUp .85s var(--ease) both;
    animation-delay: calc(.12s + var(--i, 0) * .1s);
  }
  /* ⚠️ THE HOME PAGE ORDER IS THE LOGO FIRST, then everything under it.
     Pere asked for the mark to turn and *then* settle above the slogan, so the
     rest of the hero must not already be on screen while it is still turning.
     These delays are tuned to `logo-in` (100ms + 1050ms ≈ lands at 1.15s) and
     must move with it if that duration ever changes. The page-header path above
     keeps its own faster timings — there is no logo on those pages. */
  .js .hero .rule  { animation: draw .7s var(--ease) both;   animation-delay: .95s; }
  .js .slogan--hero { animation: fadeUp .8s var(--ease) both; animation-delay: .70s; }
  .js .phead .rule { animation: draw .7s var(--ease) both;   animation-delay: .5s; }
  .js .phead__lead { animation: fadeUp .8s var(--ease) both; animation-delay: .62s; }
  .js .hero__lead  { animation: fadeUp .8s var(--ease) both; animation-delay: 1.06s; }
  .js .hero__acts  { animation: fadeUp .8s var(--ease) both; animation-delay: 1.18s; }

  /* ⚠️ The creed's line reveal is NOT here. It lives in §2.6 and §2.4 below,
     alongside `[data-lines]`, because that is the only place either path is
     wired up. Putting a `translate: 110%` here — outside both `@supports`
     branches — pushed the three lines out of their own clip boxes and waited
     for an `.is-in` class that Chrome never adds, because main.js only installs
     the IntersectionObserver when `animation-timeline: view()` is MISSING.
     The band rendered completely blank. Caught by probing the live page, not by
     reading the CSS: no checker looks for text that is present but pushed out
     of an `overflow: hidden` parent. */
}
@keyframes lineUp { from { translate: 0 110%; } to { translate: 0 0; } }
@keyframes fadeUp { from { opacity: 0; translate: 0 14px; } to { opacity: 1; translate: 0 0; } }
@keyframes draw   { from { scale: 0 1; } to { scale: 1 1; } }
@keyframes settle { from { opacity: 0; translate: 0 18px; } to { opacity: 1; translate: 0 0; } }
@keyframes wipe   { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0 0 0 0); } }

/* — 2.6 native scroll-driven: the preferred path ————————————————————— */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    [data-reveal] {
      animation: settle .6s ease-out both;
      animation-timeline: view();
      animation-range: entry 8% cover 26%;
    }
    /* the section rules draw themselves as their head arrives */
    .act .rule {
      animation: draw .6s var(--ease) both;
      animation-timeline: view(); animation-range: entry 10% cover 22%;
    }
    /* section headings, line by line, each one a little later than the last.
       `.creed__t` rides the same rule: it uses `class="lines"` rather than
       `[data-lines]` (its lines are authored, not measured), so it has to be
       named explicitly or it gets no reveal at all. */
    [data-lines] .ln > i, .creed__t .ln > i {
      animation: lineUp .8s var(--ease) both;
      animation-timeline: view();
      animation-range: entry calc(6% + var(--i, 0) * 5%) cover calc(26% + var(--i, 0) * 5%);
    }
    /* 5.4 the map wipes open from the top edge */
    [data-wipe] {
      animation: wipe 1s ease-out both;
      animation-timeline: view(); animation-range: entry 12% cover 40%;
    }
    /* 2.5 the card's particle waves drift apart as the hero leaves */
    .hero__field-l { animation: driftL linear both;
                     animation-timeline: view(); animation-range: entry 100% exit 100%; }
    .hero__field-r { animation: driftR linear both;
                     animation-timeline: view(); animation-range: entry 100% exit 100%; }
  }
}
@keyframes driftL { to { translate: -70px -50%; opacity: .25; } }
@keyframes driftR { to { translate:  70px -50%; opacity: .25; } }

/* — 2.4 the IntersectionObserver fallback, and only where 2.6 is missing — */
@supports not (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .js [data-reveal] { opacity: 0; translate: 0 18px;
                        transition: opacity .7s ease, translate .7s var(--ease); }
    .js [data-reveal].is-in { opacity: 1; translate: 0 0; }

    .js [data-lines] .ln > i, .js .creed__t .ln > i {
                               translate: 0 110%;
                               transition: translate .8s var(--ease);
                               transition-delay: calc(var(--i, 0) * .08s); }
    .js [data-lines].is-in .ln > i, .js .creed__t.is-in .ln > i { translate: 0 0; }

    .js [data-wipe] { clip-path: inset(0 0 100% 0); transition: clip-path 1s ease-out; }
    .js [data-wipe].is-in { clip-path: inset(0 0 0 0); }
  }
}

/* ── 18 · reduced motion — everything above collapses ──────────────────── */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  .progress { display: none; }
  [data-reveal] { opacity: 1 !important; translate: none !important; }
  [data-wipe] { clip-path: none !important; }
  .ln > i { translate: none !important; }
  .rule { scale: 1 1 !important; }
  .card:nth-child(n) { margin-top: 0; }
  .btn:hover, .card:hover, .org__person:hover { transform: none; }
}

/* ── 19 · print ────────────────────────────────────────────────────────── */
@media print {
  .hdr, .progress, .hero__field, .phead__field, .brief__field,
  .q__send, .hero__scroll, .reserved__slot { display: none; }
  body, .act--night, .foot, .hero, .phead { background: #fff; color: #000; }
  .q.is-stepped .q__set { display: block; }
  .ln { overflow: visible; }
}
