/* ==========================================================================
   E — Anchored

   The identity card is the head of the sticky rail, and it is already in its
   final place at its final size at scroll 0.  The hero is therefore the card
   standing on a landscape; scrolling changes only the world around it.
   Nothing morphs, so nothing here depends on scroll-driven animation being
   supported — the rail's lower panels and the guide's modules simply arrive.

   The landscape is ONE fixed full-bleed backdrop for the whole page, and the
   guide is printed on translucent vellum plates over it.

   Layer order, and what lives where:
     reset     — normalisation
     tokens    — custom properties (palette, type, the artwork geometry)
     base      — element defaults
     layout    — the fixed backdrop, the masthead, the shell, the rail
     modules   — the component system (timeline, panels, chips, specs)
     motion    — ARRIVAL ONLY.  Every animation on the page is in here, and
                 every one of them is decoration: with the layer deleted the
                 page is identical, just already assembled.
     utilities — helpers, print

   No build step, no framework, no external request.
   ========================================================================== */

@layer reset, tokens, base, layout, modules, motion, utilities;

/* --------------------------------------------------------------------------
   Lato, self-hosted — loading it from fonts.googleapis.com sends the
   visitor's IP to Google on every request, which needs consent under GDPR
   (LG Muenchen I, 3 O 17493/20).  Two weights, each ONE file subset to a
   character set chosen for the content that may be written later, not the
   content on the page today: Basic Latin, Latin-1 Supplement (German
   umlauts and ß included), and typographic punctuation.  A character
   outside it falls back to the next font silently, so the set is broad on
   purpose.  `unicode-range` states what the file really contains.  The
   subset command is in NOTES.md.

   NOT COVERED: Hungarian ő ű Ő Ű.  This Lato is "Version 1.104; Western+
   Polish" and has no such glyphs to subset — see NOTES.md.
   -------------------------------------------------------------------------- */
@font-face {
  font-family: "Lato";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/lato-400.woff2") format("woff2");
  unicode-range: U+0020-007E, U+00A0-00FF, U+2013-2014, U+2018-2019, U+201C-201D, U+2022, U+2026, U+20AC, U+2122;
}
@font-face {
  font-family: "Lato";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("fonts/lato-700.woff2") format("woff2");
  unicode-range: U+0020-007E, U+00A0-00FF, U+2013-2014, U+2018-2019, U+201C-201D, U+2022, U+2026, U+20AC, U+2122;
}

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

  * { margin: 0; }

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

  img, svg { display: block; max-inline-size: 100%; }
  img { block-size: auto; }

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

  a { color: inherit; }

  button {
    font: inherit;
    color: inherit;
    background: none;
    border: 0;
    cursor: pointer;
  }

  summary { cursor: pointer; }
  summary::-webkit-details-marker { display: none; }
}

@layer tokens {
  :root {
    color-scheme: light dark;

    /* --- C's 4px grid ------------------------------------------------- */
    --u: 4px;
    --rule: 2px;
    --hair: 1px;
    --gap-1: 4px;
    --gap-2: 8px;
    --gap-3: 12px;
    --gap-4: 16px;
    --gap-6: 24px;
    --gap-8: 32px;
    --gap-12: 48px;
    --gap-16: 64px;
    --shadow: var(--u) var(--u) 0 var(--c-shadow);
    --shadow-sm: 2px 2px 0 var(--c-shadow);

    /* ==================================================================
       THE ARTWORK.  528 x 330 px, 64 colours, pixel art with its own sky.
       One fixed backdrop for the whole page, top edge on the top of the
       viewport, only ever drawn at a whole-number scale, never smoothed.
       ================================================================== */
    --art-w: 528px;
    --art-h: 330px;

    /* COVER, ROUNDED UP.  The drawn width is the smallest whole multiple
       of 528px that covers the viewport width AND whose height (330px per
       step) covers the viewport height — so the scale is a whole number,
       and whatever does not fit is cropped, never fitted.  Written as a
       length rather than a scale factor so no unit division is needed:
       `528n >= 1.6 x vh` is the same condition as `330n >= vh`.  The
       height is exactly 0.625 of the width (5/8, exact in binary), so it
       is always a whole multiple of 330px too.  `lvh`, not `dvh`: a phone
       whose URL bar retracts must not change scale mid-scroll.          */
    --art-dw: max(round(up, 100vw, 528px), round(up, 100lvh * 1.6, 528px));
    --art-dh: calc(var(--art-dw) * 0.625);

    /* THE CROP.  Vertically none at the top, ever: the image is positioned
       at 0.  Horizontally the anchor is 16% — a focal point on the hiker
       on the ledge (source x 80-133), the one human in the scene.  On a
       desktop the crop is under 200px and the anchor barely shows; on a
       tall phone the image is 4x wider than the screen, and 16% puts him
       on the left third of the frame at 360, 390, 412 and 768 alike,
       looking into it.  The price is the town and the waterfall on phones;
       see NOTES.md for why that is the right one to pay.
       Rounded toward zero to a whole ART pixel (one step = drawn width /
       528): a 16% offset is almost never one, and a fractional offset puts
       nearest-neighbour columns at uneven widths — the smear whole-number
       scaling exists to prevent.  And a LENGTH, not `16%`: a percentage
       resolves against the positioning area, which for `.horizon` is its
       box and for a `fixed` plate background is the viewport, and the two
       differ by the scrollbar — measured as a 1px disagreement at 1440.
       A length from the left edge is the same number in both.            */
    --art-step: calc(var(--art-dw) / 528);
    --art-x: round(to-zero, calc((100vw - var(--art-dw)) * .16), var(--art-step));
    --art-y: 0px;

    /* --- palette (C's) ------------------------------------------------ */
    --c-paper:   light-dark(#f2efe6, #0d1015);
    --c-panel:   light-dark(#fbf9f4, #161b22);
    --c-panel-2: light-dark(#eeeade, #1b212a);
    --c-ink:     light-dark(#15191d, #e7ecf1);
    --c-ink-2:   light-dark(#4d545b, #9aa7b4);
    /* Re-darkened / re-lightened from d-base: on flat paper #5f676e was
       exactly AA at 11px, and a plate over the artwork's darkest pixel is
       not flat paper.  These clear 4.9:1 against the worst composite the
       vellum can produce in either theme.  Measured, see NOTES.md. */
    --c-ink-3:   light-dark(#585f65, #8b97a4);
    --c-edge:    light-dark(#1d232a, #46525f);
    --c-hair:    light-dark(#d7d1c1, #232b35);
    --c-shadow:  light-dark(#d5cfbe, #05070a);
    --c-accent:  light-dark(#0a6152, #4fd6b4);
    --c-accent-ink: light-dark(#074a3f, #7ee3c8);
    --c-focus:   light-dark(#0b5f9e, #7dd3fc);

    /* --- the vellum ---------------------------------------------------
       Decision 3 puts the landscape behind the whole guide, so there is no
       paper sheet to hide it under.  Every outermost surface is instead a
       translucent plate: `--veil` of the panel colour over a *blurred* copy
       of the same backdrop, painted by the plate itself and pinned to the
       viewport so it lines up with the sharp one to the pixel.

       Blurred, not raw: unblurred pixel art at 10% under 13px type is
       legible but visibly noisy, which is the thing decision 3 must not
       cost.  `backdrop-filter` was the obvious way to get the blur and it
       measured three times the frame cost of the other variants (see
       NOTES.md), because every plate becomes its own backdrop root.  A
       pre-blurred copy of the artwork costs 3 KB and nothing per frame.

       Nested accents (tags, notes inside panels, spec rows) are opaque:
       they sit on a plate, not on the art.  The alphas are measured.      */
    --veil: 90%;
    --veil-2: 93%;
    --surface:   color-mix(in srgb, var(--c-panel) var(--veil), transparent);
    --surface-2: color-mix(in srgb, var(--c-panel-2) var(--veil-2), transparent);
    --plate: var(--surface);

    /* The ground colour is the image's mean colour, measured: it is only
       ever seen before the image decodes or past a rubber-band overscroll. */
    --ground-day: #427c7a;
    --ground-night: #163760;

    --scene-day: url("img/bg-day.webp");
    --scene-night: url("img/bg-night.webp");

    /* The same artwork gaussian-blurred and kept at a third of its size
       (176x110) so the browser's own upscale finishes the blur.  It is
       what the plates show through themselves. */
    --soft-day: url("img/soft2-day.webp");
    --soft-night: url("img/soft2-night.webp");

    --scene: var(--scene-day);
    --soft: var(--soft-day);
    --ground: var(--ground-day);
    --logo-filter: brightness(0);

    /* --- type --------------------------------------------------------- */
    --font: "Lato", system-ui, -apple-system, "Segoe UI", sans-serif;
    --fs-xs: 0.6875rem;
    --fs-sm: 0.8125rem;
    --fs-base: clamp(0.9375rem, 0.915rem + 0.11vw, 1rem);
    --fs-md: clamp(1rem, 0.97rem + 0.15vw, 1.0625rem);
    --fs-lg: clamp(1.0625rem, 1.01rem + 0.28vw, 1.25rem);
    --fs-xl: clamp(1.25rem, 1.14rem + 0.55vw, 1.625rem);
    --fs-2xl: clamp(2.25rem, 1.36rem + 3.9vw, 4.25rem);
    --tracking-label: 0.14em;

    /* --- shell -------------------------------------------------------- */
    --gutter: clamp(16px, 3vw, 40px);
    /* Narrower than d-base's 1680: the shell has to leave real landscape
       standing to its left and right at 1440 and above, or decision 3 is
       just an expensive way to draw a texture. */
    --shell-max: 1280px;
    --rail-w: 18rem;
    --rail-gap: 40px;
    --topbar-h: 3.375rem;

    /* Where the card's top edge is, measured from the top of the viewport.
       It is the shell's own top padding AND the rail's sticky offset, which
       is what makes the card land on exactly the same pixel at scroll 0 as
       at the bottom of the page.  Change it in one place or not at all. */
    --rail-top: calc(var(--topbar-h) + var(--gap-4));
  }

  /* On a real rail the masthead is empty — the rail carries the contents,
     the links and the appearance control — so it is not drawn at all, and
     the card moves up into the space it was holding. */
  @media (min-width: 62rem) {
    :root { --rail-top: var(--gutter); }
  }

  /* A 284px card is a postage stamp on a 2560px screen.  The card's size is
     fixed for a given viewport, never during a scroll, so it may step. */
  @media (min-width: 1900px) {
    :root { --rail-w: 21rem; --shell-max: 1400px; }
  }

  /* A short desktop buys the rail its missing rows back from the margin
     above the card.  `--rail-top` is still ONE length used in two places,
     which is the whole basis of the card never moving. */
  @media (min-width: 62rem) and (height <= 56rem) {
    :root { --rail-top: 24px; }
  }

  /* light-dark() carries colours only; the day/night artwork and the logo
     flatten run off prefers-color-scheme plus [data-theme]. */
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
      --scene: var(--scene-night);
      --soft: var(--soft-night);
      --ground: var(--ground-night);
      --logo-filter: brightness(0) invert(1);
    }
  }

  /* PHONE PAN.  Where the rail has collapsed, app.js walks the backdrop
     across the scene one step per section.  `--pan-x` / `--pan-y` are the
     REST offsets, whole art pixels, and they are what both the backdrop
     layer and every plate's blurred copy read.  The slide between two rest
     states is not done here — see `.horizon__art` and @layer motion. */
  :root.has-pan {
    --art-x: round(nearest, var(--pan-x), var(--art-step));
    --art-y: round(nearest, var(--pan-y), var(--art-step));
  }

  :root[data-theme="light"] { color-scheme: light; }

  :root[data-theme="dark"] {
    color-scheme: dark;
    --scene: var(--scene-night);
    --soft: var(--soft-night);
    --ground: var(--ground-night);
    --logo-filter: brightness(0) invert(1);
  }
}

@layer base {
  html {
    scroll-padding-block-start: calc(var(--rail-top) + var(--gap-4));
    /* Seen only before the image decodes or past an overscroll. */
    background-color: var(--ground);
  }

  @media (prefers-reduced-motion: no-preference) {
    html { scroll-behavior: smooth; }
  }

  body {
    margin: 0;
    background-color: transparent;
    color: var(--c-ink);
    font-family: var(--font);
    font-size: var(--fs-base);
    line-height: 1.6;
    font-synthesis-weight: none;
    text-rendering: optimizeLegibility;
    -webkit-font-smoothing: antialiased;
  }

  h1, h2, h3, h4 {
    font-weight: 700;
    line-height: 1.18;
    text-wrap: balance;
  }

  p { text-wrap: pretty; }

  a {
    color: var(--c-accent);
    text-decoration-thickness: 1px;
    text-underline-offset: 0.18em;
    overflow-wrap: anywhere;

    &:hover { color: var(--c-accent-ink); }
  }

  strong { font-weight: 700; }
  time { font-variant-numeric: tabular-nums; }

  :focus-visible {
    outline: var(--rule) solid var(--c-focus);
    outline-offset: var(--rule);
    border-radius: 1px;
  }

  /* Every focusable thing on this page sits on a vellum plate, so one dark
     ring is correct everywhere. */

  ::selection { background: color-mix(in oklab, var(--c-accent) 28%, transparent); }
}

@layer layout {
  .skip {
    position: fixed;
    inset-block-start: var(--gap-2);
    inset-inline-start: var(--gap-2);
    z-index: 120;
    padding: var(--gap-2) var(--gap-3);
    background: var(--c-panel);
    border: var(--rule) solid var(--c-edge);
    box-shadow: var(--shadow);
    color: var(--c-ink);
    font-weight: 700;
    translate: 0 -300%;

    &:focus-visible { translate: 0 0; }
  }

  /* ---------------------------------------------------------- horizon ---
     ONE fixed full-bleed landscape, behind everything, for the whole page:
     its top edge on the top of the viewport, cover at a whole-number
     scale, the overflow cropped off the bottom and the sides.  Nothing
     about it is scroll-driven — it is simply the backdrop, and the guide
     is what moves over it. ------------------------------------------- */
  .horizon {
    position: fixed;
    inset: 0;
    z-index: 0;
    overflow: hidden;
    pointer-events: none;
    background-color: var(--ground);
  }

  /* The artwork is its own layer, placed with a transform rather than a
     background position, so that the phone pan can move it on the
     compositor without repainting a full screen of pixel art per frame
     (measured: repainting it dropped a third of the frames at 4x CPU).
     At rest the transform is the same whole-art-pixel offset the plates'
     copies use, from the same origin — the viewport's top-left corner. */
  .horizon__art {
    position: absolute;
    inset-block-start: 0;
    inset-inline-start: 0;
    inline-size: var(--art-dw);
    block-size: var(--art-dh);
    background: var(--scene) 0 0 / 100% 100% no-repeat;
    image-rendering: pixelated;
    transform: translate(var(--art-x), var(--art-y));
  }

  :root.has-pan .horizon__art { will-change: transform; }

  /* ----------------------------------------------------------- topbar ---
     A phone fixture only.  It carries the section index (A had none) and
     the appearance control, and it is where the card shrinks to — decision
     5, and the one place on the page where the card changes size.  On a
     real rail all three of those live in the rail, so the masthead has
     nothing to say and is not drawn. -------------------------------- */
  .topbar {
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    z-index: 100;
    block-size: var(--topbar-h);
    /* Opaque, and the only surface on the page that is.  A masthead is not
       a plate over the landscape: what passes behind it is the guide's own
       text, and a translucent bar over scrolling body copy is unreadable
       at any alpha.  (It was translucent for one build.  You could read
       "accounting platform for Zukunftswerk eG" through the name.) */
    background: var(--c-panel);
    color: var(--c-ink);
    box-shadow: 0 var(--hair) 0 var(--c-hair);
  }

  @media (min-width: 62rem) {
    .topbar { display: none; }
  }

  .topbar__inner {
    inline-size: min(100% - 2 * var(--gutter), var(--shell-max));
    margin-inline: auto;
    block-size: 100%;
    display: flex;
    align-items: center;
    gap: var(--gap-2);
  }

  /* THE CARD, SHRUNK — decision 5, and the one place on the page where the
     identity changes size.  It is the card's own three things (portrait,
     name, role) at masthead scale, and it arrives exactly as the card's
     last pixel leaves, so the two are never both on screen.

     Which of the three fit is a measured question, not a taste one: the
     masthead's other tenants (Contents, the appearance control) leave
     148px at 390px of viewport and 118px at 360px.  Nothing is ever
     truncated — a masthead is no place for an ellipsis, and every one of
     these words is in full in the card. */
  .topbar__name {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: center;
    gap: var(--gap-2);
    min-inline-size: 0;
    text-decoration: none;
    color: inherit;
    transform-origin: left center;

    & img {
      display: none;
      inline-size: 24px;
      block-size: 24px;
      object-fit: cover;
      border: var(--hair) solid var(--c-edge);
    }
  }

  /* The three steps, each the width at which that part fits whole.
     Budget for the chip is `viewport - 227px` (gutters, Contents, the
     appearance control, two gaps); the parts cost 115px for the name,
     +32px for the portrait, +95px for the role. */
  @media (min-width: 23rem) {
    .topbar__name { grid-template-columns: 24px minmax(0, 1fr); }
    .topbar__name img { display: block; }
  }

  .topbar__id {
    display: grid;
    min-inline-size: 0;
  }

  .topbar__id-name {
    font-size: var(--fs-xs);
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: .01em;
    white-space: nowrap;
  }

  /* The role joins the chip only where the whole line fits (30rem up). */
  .topbar__id-role {
    display: none;
    font-size: .625rem;
    line-height: 1.25;
    letter-spacing: .01em;
    color: var(--c-ink-2);
    white-space: nowrap;
  }

  @media (min-width: 30rem) {
    .topbar__id-role { display: block; }
  }

  :root.has-js .topbar__name {
    opacity: 0;
    pointer-events: none;
  }

  :root.has-js .topbar.is-pinned .topbar__name {
    opacity: 1;
    pointer-events: auto;
  }

  .topbar__spacer { margin-inline-start: auto; }

  /* ------------------------------------------------------------ shell ---
     One grid for the whole page.  The rail is column 1 and spans both rows,
     so its sticky box is the full height of the document; the hero is the
     first row of column 2 and holds nothing at all.

     `padding-block-start` is `--rail-top`, the same length the rail sticks
     at.  That is the whole trick: the card's static position and its stuck
     position are the same pixel, so it is pinned from scroll 0 and never
     moves. ----------------------------------------------------------- */
  .shell {
    position: relative;
    z-index: 2;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--gap-8);
    inline-size: min(100% - 2 * var(--gutter), var(--shell-max));
    /* Centred, but on a WHOLE pixel.  `margin-inline: auto` puts the shell
       at x 72.5 whenever the viewport minus the scrollbar is odd, and
       Chromium snaps a `fixed` background relative to a fractionally
       placed element: every plate's blurred backdrop sat 1px off the real
       one at 1440.  Measured; rounding the left margin to a whole pixel fixed it
       (nearest, so the card keeps the x 73 it was verified at). */
    margin-inline-start: round(nearest, calc((100% - min(100% - 2 * var(--gutter), var(--shell-max))) / 2), 1px);
    margin-inline-end: 0;
    /* The tail padding belongs to `main`, not to the shell: the rail's
       sticky containing block is its grid area, and any shell padding
       below that area is room the card gets pushed up by in the last
       screenful.  Measured — the card lost 24px at the document end
       before this moved. */
    padding-block: var(--rail-top) 0;
  }

  @media (min-width: 62rem) {
    .shell {
      grid-template-columns: var(--rail-w) minmax(0, 1fr);
      grid-template-rows: auto minmax(0, 1fr);
      column-gap: var(--rail-gap);
      row-gap: 0;
      align-items: start;
    }
    .hero { grid-column: 2; grid-row: 1; }
    .rail { grid-column: 1; grid-row: 1 / span 2; }
    main   { grid-column: 2; grid-row: 2; }
  }

  /* ------------------------------------------------------------- hero ---
     The first screen holds the identity card and nothing else, so the hero
     is literally empty: it is the height that keeps the first module below
     the fold.  `--rail-top` is already the shell's top padding, so this is
     exactly one viewport. ------------------------------------------- */
  .hero {
    block-size: calc(100svh - var(--rail-top));
    pointer-events: none;
  }

  @media (max-width: 61.999rem) {
    /* On a phone the card is the first thing in the flow, so the hero's
       job is done by a minimum height on the rail instead. */
    .hero { display: none; }
  }

  main {
    container-type: inline-size;
    container-name: main;
    display: flex;
    flex-direction: column;
    gap: var(--gap-12);
    min-inline-size: 0;
    padding-block-end: var(--gap-16);
  }

  /* A phone's page ends on open landscape.  The pan's last step brings the
     art's bottom-right corner into view, and with it the rabbit in the grass
     — whose top sits 55 art rows above the art's bottom edge.  Without room
     after the footer, the footer is exactly what covers him.  Measured in
     art rows, so it holds at every scale. */
  @media (max-width: 61.999rem) {
    main { padding-block-end: calc(56 * var(--art-step) + var(--gap-6)); }
  }

  .prose > * { max-inline-size: 68ch; }

  .split {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--gap-6);
  }

  @container main (min-width: 54rem) {
    .split {
      grid-template-columns: minmax(0, 1.7fr) minmax(15rem, 1fr);
      align-items: start;
    }
  }
}

@layer modules {

  /* ----------------------------------------------------------- vellum ---
     PROBLEM 1.  Every outermost surface on the page is one of these: a
     translucent plate of panel colour over a blurred, viewport-pinned copy
     of the backdrop.  Nothing else gets a background, so the landscape is
     sharp everywhere between them — the gutters, the gaps between modules,
     the whole column beside the rail, and the hero.

     The blur is what makes the alpha safe.  It removes the artwork's high
     frequencies, so what a plate shows is the art's *local colour*, not its
     pixels: you read the landscape through the guide instead of reading it
     as texture behind the type.  The composite over the busiest part of the
     image stays within a few points of the flat panel colour — measured,
     not guessed; the numbers are in NOTES.md. */
  .idcard,
  .linklist,
  .toc:not(.toc--menu),
  .appearance,
  .section__head,
  .panel,
  .note,
  .entry,
  .specs,
  .contact__item,
  .footer {
    /* Two layers, both pinned to the viewport, so a plate paints exactly
       what is behind it: the veil over a blurred copy of the backdrop at
       the same size and the same anchor as `.horizon` — which is why the
       two line up to the pixel.  The flat colour underneath is the panel,
       so a browser that treats `fixed` as `scroll` degrades to a plain
       paper plate instead of to nonsense. */
    background-color: var(--c-panel);
    background-image:
      linear-gradient(var(--plate), var(--plate)),
      var(--soft);
    background-attachment: fixed;
    background-repeat: no-repeat;
    background-size: auto, var(--art-dw) var(--art-dh);
    background-position: 0 0, var(--art-x) var(--art-y);
  }

  .note { --plate: var(--surface-2); }

  /* ------------------------------------------------------ phone menu ---
     Native <details>, so the section index works with JS off.  Hidden once
     the rail is a real rail. ----------------------------------------- */
  .menu { position: relative; }

  .menu__btn {
    display: inline-flex;
    align-items: center;
    gap: var(--gap-2);
    padding: var(--gap-1) var(--gap-2);
    border: var(--rule) solid currentColor;
    border-color: color-mix(in srgb, currentColor 45%, transparent);
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    list-style: none;

    &::after {
      content: "";
      inline-size: 0;
      block-size: 0;
      border-inline: 4px solid transparent;
      border-block-start: 5px solid currentColor;
    }
  }

  .menu[open] .menu__btn::after { rotate: 180deg; }

  .menu__panel {
    position: absolute;
    inset-block-start: calc(100% + var(--gap-2));
    inset-inline-end: 0;
    z-index: 10;
    min-inline-size: 14rem;
    padding: var(--gap-2);
    background: var(--c-panel);
    border: var(--rule) solid var(--c-edge);
    box-shadow: var(--shadow);
    color: var(--c-ink);
  }

  @media (min-width: 62rem) {
    .menu { display: none; }
  }

  /* ------------------------------------------------------------ rail ---
     Desktop: C's sticky identity rail.
     Phone: a compact header — the id card stays, everything else folds
     into one disclosure, so the page itself is above the fold.  C stacked
     four panels here and pushed the content off the screen. ---------- */
  .rail {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--gap-4);
    min-inline-size: 0;
  }

  /* Phone: the card is the hero.  A minimum height of one screen keeps the
     first module below the fold without a spacer element, and the card
     stays at the top of it. */
  @media (max-width: 61.999rem) {
    .rail {
      justify-content: start;
      min-block-size: calc(100svh - var(--rail-top) - var(--gap-8));
    }
  }

  /* Desktop: the sticky offset is the same length as the shell's top
     padding, so the card is already at its stuck position at scroll 0.
     It never moves and it never resizes.  `max-block-size` + `overflow`
     keep a short viewport from cutting the appearance control off. */
  @media (min-width: 62rem) {
    .rail {
      position: sticky;
      inset-block-start: var(--rail-top);
      gap: var(--gap-3);
      /* The rail may come closer to the bottom edge than to the top one —
         the card's offset is fixed, the tail is not.  At 900px tall the
         whole rail fits with room to spare; below that it scrolls, which is
         why the appearance control is last and the card is first.  The
         scrollbar itself stays hidden: wheel, touch and focus still reach
         the bottom of the rail. */
      max-block-size: calc(100dvh - var(--rail-top) - var(--gap-4));
      overflow: auto;
      overscroll-behavior: contain;
      scrollbar-width: none;
      padding-inline-end: var(--gap-1);
    }
    .rail::-webkit-scrollbar { display: none; }
  }

  /* The card.  One size, one position, scroll 0 to the bottom of the page.
     Everything about it is fixed: no clamp() on its type, no container
     query, no width that tracks the viewport inside a breakpoint. */
  .idcard {
    display: grid;
    grid-template-columns: 72px minmax(0, 1fr);
    gap: var(--gap-4);
    align-items: start;
    padding: var(--gap-4);
    border: var(--rule) solid var(--c-edge);
    box-shadow: var(--shadow);
  }

  .idcard__portrait {
    inline-size: 72px;
    block-size: 72px;
    object-fit: cover;
    border: var(--rule) solid var(--c-edge);
    background: var(--c-panel-2);
  }

  @media (min-width: 62rem) {
    .idcard { grid-template-columns: minmax(0, 1fr); justify-items: start; }
    .idcard__portrait { inline-size: 104px; block-size: 104px; }
  }

  @media (min-width: 1900px) {
    .idcard__portrait { inline-size: 132px; block-size: 132px; }
  }

  .idcard__name {
    font-size: var(--fs-xl);
    letter-spacing: -0.015em;
  }

  .idcard__role {
    margin-block-start: var(--gap-1);
    color: var(--c-ink-2);
    font-size: var(--fs-sm);
    font-weight: 400;
    line-height: 1.45;
  }

  /* A's tl;dr line, now inside the card — the hero has no other type.  */
  .idcard__tldr {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--gap-1) var(--gap-2);
    margin-block-start: var(--gap-3);
    padding-block-start: var(--gap-3);
    border-block-start: var(--hair) solid var(--c-hair);
    font-size: var(--fs-sm);

    & a { font-weight: 700; }
  }

  .idcard__tldr-key {
    color: var(--c-ink-3);
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

  .sep { color: var(--c-ink-3); }

  /* The card spans the full card on a phone, where the portrait is beside
     the name rather than above it. */
  @media (max-width: 61.999rem) {
    .idcard__tldr { grid-column: 1 / -1; }
    /* A tablet's shell is wide enough to stretch the card into a banner. */
    .idcard { max-inline-size: 34rem; }
  }

  .label {
    display: inline-block;
    color: var(--c-ink-3);
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

  /* The rail's lower panels exist on a real rail only.  C stacked four of
     them above the content on a phone and pushed the page below the fold;
     both are duplicated elsewhere for narrow screens (the contents in the
     masthead menu, the links in the Contact section), so on a phone the
     rail is one compact id card and nothing else. */
  .rail__panels { display: none; }

  @media (min-width: 62rem) {
    .rail__panels {
      display: grid;
      gap: var(--gap-6);
    }
  }

  /* ---------------------------------------------------- appearance --- */
  .appearance {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--gap-3);
    padding: var(--gap-2) var(--gap-3);
    border: var(--rule) solid var(--c-edge);
    box-shadow: var(--shadow);
  }

  /* A 900px-tall viewport has room for the rail at its normal rhythm; an
     800px one does not, and `overflow: auto` is a reachability net, not a
     layout.  Tighten the three lists (never the card's type) so the whole
     rail still fits without a scrollbar on the shorter common desktop.
     Below roughly 780px of viewport it genuinely scrolls — which is why
     the card is the rail's first child and the appearance control its
     last. */
  @media (min-width: 62rem) and (height <= 56rem) {
    .rail { gap: var(--gap-2); }
    .idcard__portrait { inline-size: 64px; block-size: 64px; }
    .rail__panels { gap: var(--gap-3); }
    .linklist > li > a { padding-block: 2px; }
    .toc__list > li > a { padding-block: 2px; }
    .toc__title { padding-block-end: 2px; }
  }

  /* -------------------------------------------------------- linklist --- */
  .linklist {
    display: grid;
    gap: var(--rule);
    padding: var(--gap-2);
    border: var(--rule) solid var(--c-edge);
    box-shadow: var(--shadow);
  }

  .linklist > li > a {
    display: grid;
    grid-template-columns: 20px minmax(0, 1fr);
    gap: var(--gap-3);
    align-items: center;
    padding: var(--gap-1) var(--gap-2);
    color: var(--c-ink);
    font-size: var(--fs-sm);
    text-decoration: none;

    & svg { inline-size: 20px; block-size: 20px; color: var(--c-ink-3); }

    &:hover {
      background: var(--c-panel-2);
      color: var(--c-accent-ink);
      & svg { color: var(--c-accent); }
    }
  }

  /* ------------------------------------------------------------- toc --- */
  .toc {
    padding: var(--gap-2);
    border: var(--rule) solid var(--c-edge);
    box-shadow: var(--shadow);
    counter-reset: toc;
  }

  .toc--menu {
    padding: 0;
    background: none;
    border: 0;
    box-shadow: none;
  }

  .toc__title {
    display: block;
    padding: 0 var(--gap-2) var(--gap-1);
  }

  .toc__list > li > a {
    display: grid;
    grid-template-columns: 2.25em minmax(0, 1fr);
    padding: var(--gap-1) var(--gap-2);
    color: var(--c-ink-2);
    font-size: var(--fs-sm);
    text-decoration: none;
    counter-increment: toc;

    &::before {
      content: counter(toc, decimal-leading-zero);
      color: var(--c-ink-3);
      font-variant-numeric: tabular-nums;
    }

    &:hover { background: var(--c-panel-2); color: var(--c-ink); }

    &[aria-current="true"] {
      background: var(--c-panel-2);
      box-shadow: inset var(--rule) 0 0 var(--c-accent);
      color: var(--c-ink);
      font-weight: 700;

      &::before { color: var(--c-accent); }
    }
  }

  .toc--menu .toc__list > li > a:hover,
  .toc--menu .toc__list > li > a[aria-current="true"] { background: var(--c-paper); }

  /* ---------------------------------------------------------- toggle ---
     One instance, in the masthead, so it is reachable at every width
     without duplicating an interactive control. ---------------------- */
  .topbar__theme {
    display: inline-flex;
    align-items: center;
    gap: var(--gap-2);
    padding: var(--gap-1) var(--gap-2);
    border: var(--rule) solid currentColor;
    border-color: color-mix(in srgb, currentColor 45%, transparent);
    color: inherit;
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;

    &:hover { border-color: color-mix(in srgb, currentColor 80%, transparent); }
  }

  .topbar__theme-dot {
    inline-size: var(--gap-2);
    block-size: var(--gap-2);
    background: linear-gradient(135deg, currentColor 0 50%, transparent 50% 100%);
    box-shadow: inset 0 0 0 1px currentColor;
  }

  :root[data-theme="light"] .topbar__theme-dot { background: currentColor; }
  :root[data-theme="dark"] .topbar__theme-dot { background: transparent; }

  /* --------------------------------------------------------- section ---
     <main> is a query container, which implies style containment: a
     counter it resets itself is not in scope for its descendants, and a
     counter created outside it does not reach in either.  Creating it on
     the first section fixes both — counter scope covers later siblings. */
  main > .section:first-of-type { counter-reset: sec; }

  .section {
    display: flex;
    flex-direction: column;
    gap: var(--gap-6);
    scroll-margin-block-start: calc(var(--rail-top) + var(--gap-4));
  }

  /* The head is a plate of its own, shrunk to its words, so the landscape
     runs on past it instead of being cut by a full-width rule. */
  .section__head {
    display: inline-grid;
    align-self: start;   /* .section is a flex column: cross axis is inline */
    justify-self: start;
    max-inline-size: 100%;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: baseline;
    gap: var(--gap-4);
    counter-increment: sec;
    padding: var(--gap-2) var(--gap-4);
    border: var(--rule) solid var(--c-edge);
    box-shadow: var(--shadow-sm);

    &::before {
      content: counter(sec, decimal-leading-zero);
      color: var(--c-ink-3);
      font-size: var(--fs-sm);
      font-weight: 700;
      font-variant-numeric: tabular-nums;
      letter-spacing: var(--tracking-label);
    }
  }

  .section__title {
    font-size: var(--fs-xl);
    letter-spacing: -0.01em;
  }

  /* ----------------------------------------------------------- panel --- */
  .panel {
    padding: var(--gap-6);
    border: var(--rule) solid var(--c-edge);
    box-shadow: var(--shadow);
  }

  .stack > * + * { margin-block-start: var(--gap-4); }

  .note {
    padding: var(--gap-4) var(--gap-6);
    border: var(--rule) solid var(--c-edge);
    border-inline-start-width: 8px;
    border-inline-start-color: var(--c-accent);
    box-shadow: var(--shadow-sm);
  }

  .note p { font-size: var(--fs-md); line-height: 1.55; }

  /* ------------------------------------------------------------ tags --- */
  .tags { display: flex; flex-wrap: wrap; gap: var(--gap-2); }

  .tag {
    display: inline-block;
    padding: 2px var(--gap-2);
    border: var(--rule) solid var(--c-edge);
    background: var(--c-panel-2);
    color: var(--c-ink-2);
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    white-space: nowrap;
  }

  .tags--titled {
    align-items: center;
    margin-block-start: var(--gap-4);
    padding-block-start: var(--gap-4);
    border-block-start: var(--hair) solid var(--c-hair);
  }

  /* ---------------------------------------------------------- skills --- */
  .modules {
    display: grid;
    gap: var(--gap-6);
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
  }

  @container main (min-width: 52rem) {
    .modules { grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); }
  }

  .module__title {
    font-size: var(--fs-lg);
    margin-block-end: var(--gap-4);
    padding-block-end: var(--gap-2);
    border-block-end: var(--hair) solid var(--c-hair);
  }

  .bullets > li {
    position: relative;
    padding-inline-start: var(--gap-4);
    margin-block-start: var(--gap-2);
    break-inside: avoid;

    &::before {
      content: "";
      position: absolute;
      inset-block-start: 0.62em;
      inset-inline-start: 0;
      inline-size: var(--u);
      block-size: var(--u);
      background: var(--c-accent);
    }
  }

  .bullets > li:first-child { margin-block-start: 0; }

  .bullets ul {
    margin-block-start: var(--gap-1);

    & > li {
      position: relative;
      padding-inline-start: var(--gap-4);
      margin-block-start: var(--gap-1);
      color: var(--c-ink-2);
      font-size: calc(var(--fs-base) * 0.96);

      &::before {
        content: "";
        position: absolute;
        inset-block-start: 0.68em;
        inset-inline-start: 0;
        inline-size: var(--u);
        block-size: var(--hair);
        background: var(--c-ink-3);
      }
    }
  }

  /* -------------------------------------------------------- timeline --- */
  .timeline { display: flex; flex-direction: column; gap: var(--gap-6); }

  .entry {
    container-type: inline-size;
    container-name: entry;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--gap-4);
    padding: var(--gap-6);
    border: var(--rule) solid var(--c-edge);
    box-shadow: var(--shadow);
  }

  @container main (min-width: 44rem) {
    .entry { grid-template-columns: 11.5rem minmax(0, 1fr); gap: var(--gap-8); }
  }

  .entry__meta {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--gap-3) var(--gap-6);
    padding-block-end: var(--gap-3);
    border-block-end: var(--hair) solid var(--c-hair);
  }

  @container main (min-width: 44rem) {
    .entry__meta {
      flex-direction: column;
      align-items: start;
      gap: var(--gap-4);
      padding-block-end: 0;
      padding-inline-end: var(--gap-6);
      border-block-end: 0;
      border-inline-end: var(--hair) solid var(--c-hair);
      block-size: 100%;
    }
  }

  .entry__dates {
    color: var(--c-ink-2);
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: var(--tracking-label);
    line-height: 1.7;
    text-transform: uppercase;
    font-variant-numeric: tabular-nums;
  }

  .entry__now {
    display: inline-block;
    margin-block-start: var(--gap-1);
    padding: 1px var(--gap-2);
    border: var(--rule) solid var(--c-accent);
    color: var(--c-accent-ink);
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

  /* The company marks.  Both source PNGs carry a quarter-canvas of empty
     padding, which is what made ARMEDANGELS render as a tiny black glyph
     at 48px.  The shipped copies are cropped to the ink, drawn at 56px
     tall, and flattened to a one-colour silhouette so they read the same
     in either theme instead of being black-on-black. */
  .entry__mark {
    block-size: 56px;
    inline-size: auto;
    filter: var(--logo-filter);
    opacity: .82;
  }

  .entry__org { font-size: var(--fs-lg); letter-spacing: -0.005em; }

  .entry__meta .wordmark {
    display: block;
    inline-size: 140px;
    max-inline-size: 100%;
    block-size: auto;
    fill: currentColor;
    opacity: .82;
  }

  .entry__role {
    margin-block-start: var(--gap-1);
    color: var(--c-accent-ink);
    font-size: var(--fs-sm);
    font-weight: 700;
    letter-spacing: 0.02em;
  }

  .entry__body > .bullets { margin-block-start: var(--gap-4); }

  /* A long entry earns two columns; :has() decides, not a hand-set class. */
  @container entry (min-width: 46rem) {
    .entry:has(.bullets > li:nth-child(8)) .bullets {
      columns: 22rem 3;
      column-gap: var(--gap-12);
      column-rule: var(--hair) solid var(--c-hair);
    }
  }

  /* The "more" rule keeps its dashed, unfilled look, but it is the one
     surface whose type would otherwise sit on raw artwork, so it keeps the
     plate and drops only the shadow. */
  .entry--more {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    padding-block: var(--gap-4);
    color: var(--c-ink-2);
    letter-spacing: 0.3em;
    box-shadow: none;
    border-style: dashed;
  }

  /* ----------------------------------------------------------- specs --- */
  .specs {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    border: var(--rule) solid var(--c-edge);
    box-shadow: var(--shadow);
  }

  @container main (min-width: 40rem) {
    .specs { grid-template-columns: minmax(14rem, 24rem) minmax(0, 1fr); }
  }

  .specs__row {
    display: grid;
    grid-template-columns: subgrid;
    grid-column: 1 / -1;
    gap: var(--gap-2) var(--gap-8);
    padding: var(--gap-4) var(--gap-6);
    border-block-start: var(--hair) solid var(--c-hair);

    &:first-child { border-block-start: 0; }
  }

  .specs dt { font-size: var(--fs-md); font-weight: 700; }
  .specs dd { margin: 0; color: var(--c-ink-2); font-size: var(--fs-sm); line-height: 1.6; }

  /* --------------------------------------------------------- contact --- */
  .contact {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
    gap: var(--gap-4);
  }

  .contact__item {
    display: grid;
    grid-template-columns: 32px minmax(0, 1fr);
    align-items: center;
    gap: var(--gap-4);
    padding: var(--gap-4);
    border: var(--rule) solid var(--c-edge);
    box-shadow: var(--shadow-sm);
  }

  .contact__item .icon svg {
    inline-size: 32px;
    block-size: 32px;
    color: var(--c-accent);
  }

  .heading {
    display: block;
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
  }

  .heading a {
    color: var(--c-ink);
    text-decoration: none;

    &:hover { color: var(--c-accent-ink); text-decoration: underline; }
  }

  /* ---------------------------------------------------------- footer --- */
  .footer {
    display: flex;
    flex-wrap: wrap;
    gap: var(--gap-2) var(--gap-4);
    align-items: center;
    padding: var(--gap-4) var(--gap-6);
    border: var(--rule) solid var(--c-edge);
    box-shadow: var(--shadow-sm);
    color: var(--c-ink-2);
    font-size: var(--fs-sm);
  }
}

/* ==========================================================================
   @layer motion — ARRIVAL, and nothing else.

   Nothing on this page morphs, so nothing here is load-bearing.  Delete the
   layer and you get the same page with everything already in place; that is
   the test each rule below has to pass.

   Two arrivals:
     1. the rail's lower panels, which are not in the hero (decision 4) and
        appear as soon as the visitor leaves it;
     2. the modules, which rise over the landscape as they come into view.

   NO TRANSFORMS ON ANYTHING THAT CONTAINS A PLATE — not even `translate:
   0 0`, which is still a transform.  A transformed ancestor turns
   `background-attachment: fixed` on the plates inside it into "fixed to
   that ancestor", and the blurred copy slides off the real backdrop.  So
   the arrivals move with a relative `inset-block-start` offset instead,
   which moves the box without making it a transform, and the copy stays
   right even mid-arrival — which matters on a phone, where a section
   arriving and the backdrop panning happen at the same moment.  Measured,
   not supposed — see NOTES.md.  (The masthead chip may use a transform:
   the masthead is opaque and holds no plate.)

   The *hiding* is composition and stays outside the reduced-motion guard —
   under `reduce` the panels are still absent from the hero, they just snap
   in.  The modules are the opposite: under `reduce` they are never hidden
   at all, because a module that never fades in must never have faded out.
   ========================================================================== */
@layer motion {

  /* 1. the rail panels ---------------------------------------------------
     `visibility` and not just `opacity`: an invisible panel that still
     takes focus is a keyboard trap on the first screen.  `app.js` also
     reveals them on the first Tab, so the keyboard route to Contents and
     the appearance control never depends on having scrolled. */
  :root.has-js .rail__panels {
    opacity: 0;
    visibility: hidden;
  }

  :root.has-js.is-assembled .rail__panels {
    opacity: 1;
    visibility: visible;
  }

  @media (prefers-reduced-motion: no-preference) {
    /* The panels settle DOWN into place, from -10px, and never up from
       +10px.  A transform still counts toward scrollable overflow, so
       translating the last child of a scrollable rail downward puts 10px
       of phantom overflow in the box and Chromium draws a full-height
       scrollbar right beside the card — in the hero, at scroll 0, which is
       the worst place on the page for it.  Upward costs nothing: the
       panels start well below the rail's top edge. */
    :root.has-js .rail__panels {
      position: relative;
      inset-block-start: -10px;
      transition: opacity .4s ease, inset-block-start .4s ease, visibility 0s linear .4s;
    }
    :root.has-js.is-assembled .rail__panels {
      inset-block-start: 0;
      transition: opacity .4s ease, inset-block-start .4s ease, visibility 0s linear 0s;
    }
  }

  /* 2. the card shrinking into the masthead (phone) ----------------------
     The chip arrives a little larger and a little lower than it ends up —
     from the direction and the size of the card it replaces — and settles.
     That is the whole of "shrinks into the header": there is no morph and
     nothing is measured against anything, so under `reduce` it simply
     appears, and with the layer deleted it is simply there. */
  @media (prefers-reduced-motion: no-preference) {
    :root.has-js .topbar__name {
      scale: 1.14;
      translate: 0 7px;
      transition: opacity .26s ease, scale .26s ease, translate .26s ease;
    }
    :root.has-js .topbar.is-pinned .topbar__name {
      scale: none;
      translate: none;
    }
  }

  /* 3. the phone pan -----------------------------------------------------
     app.js moves `.horizon__art` between rest states with a stepped
     transform animation (every keyframe a whole art pixel), which runs on
     the compositor.  The plates' blurred copies cannot follow a compositor
     animation — they are fixed backgrounds, and moving those means a
     repaint per frame, the very cost being avoided — so for the length of
     the slide they are simply taken away: the veil stays, and what shows
     through it is the real backdrop, moving in exact lockstep because it IS
     the backdrop.  At the destination the copy comes back at the new
     offset.  Both swaps keep the offset; they trade blurred for sharp
     texture at 10% and move a plate's tone by under one level (measured).
     Under reduced motion there is no slide and no swap: each offset is
     simply taken. */
  :root.is-panning :is(.idcard, .linklist, .toc:not(.toc--menu), .appearance,
                       .section__head, .panel, .note, .entry, .specs,
                       .contact__item, .footer) {
    background-color: transparent;
    background-image: linear-gradient(var(--plate), var(--plate));
  }

  /* 4. the modules -------------------------------------------------------
     `.has-anim` is set by app.js only once the observer exists, so a thrown
     script or a browser without IntersectionObserver leaves every module
     visible instead of leaving the page blank. */
  @media (prefers-reduced-motion: no-preference) {
    :root.has-anim main > .section,
    :root.has-anim main > .footer {
      position: relative;
      opacity: 0;
      inset-block-start: 18px;
      transition: opacity .55s ease, inset-block-start .55s cubic-bezier(.2, .7, .3, 1);
    }

    :root.has-anim main > .is-in {
      opacity: 1;
      inset-block-start: 0;
    }
  }
}

@layer utilities {
  .visually-hidden {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  /* JS is optional: without it the toggle never appears and the OS
     preference stays in charge. */
  .js-only { display: none; }
  :root.has-js .js-only { display: flex; }

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

  @media print {
    :root { color-scheme: light; }

    .horizon, .topbar, .skip, .hero { display: none !important; }

    .shell { display: block; inline-size: auto; padding: 0; }
    .rail { position: static; max-block-size: none; overflow: visible; }
    .rail__panels { display: grid; opacity: 1; visibility: visible; }

    /* On paper there is no landscape to be translucent over, and the
       plates must not print their backdrop copy. */
    .idcard, .linklist, .toc, .appearance, .section__head, .panel,
    .note, .entry, .specs, .contact__item, .footer {
      background: #fff !important;
      background-image: none !important;
      box-shadow: none !important;
    }

    main > .section, main > .footer { opacity: 1 !important; inset-block-start: 0 !important; }
    .entry, .panel, .specs, .contact { break-inside: avoid; }
    main { gap: var(--gap-8); }
  }
}
