/* Seiteneigene Masse: die Landing-Page ist 1200px breit, die Rechtstexte
       820px. Deshalb steht --maxw hier und nicht im geteilten Stylesheet. */
    :root { --maxw: 1200px; }

    html { scroll-behavior: smooth; scroll-padding-top: 80px; -webkit-text-size-adjust: 100%; }
    body {
      font-family: var(--ff);
      background: var(--bg);
      color: var(--text-1);
      line-height: 1.65;
      font-weight: 400;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      overflow-x: hidden;
    }
    /* Subtle film grain for texture (no flat sterility) */
    body::after {
      content: ''; position: fixed; inset: 0; z-index: 9999; pointer-events: none;
      opacity: var(--grain);
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
    }
    a { color: var(--accent); text-decoration: none; }
    img { max-width: 100%; height: auto; display: block; }
    ::selection { background: var(--accent-soft); color: var(--accent-deep); }
    :focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px; border-radius: var(--r-xs); }
    /* SC 2.4.11 wants 3:1 between the ring and what it sits on. --accent clears
       that everywhere except the code plate, which is a fixed near-black in both
       themes: the light accent measured 2.85:1 there. The plate carries its own
       ring colour instead of the page's. */
    .code-wrap, .code-block { --focus-ring: var(--code-accent); }

    /* Skip link: index and install put 26 focusable nodes between the top of the
       page and the content, so keyboard users need a way past the nav. */
    .skip-link { position: absolute; left: -9999px; top: 0; z-index: 200; }
    .skip-link:focus { left: 8px; top: 8px; padding: 12px 18px; display: inline-flex; align-items: center; min-height: 44px;
      background: var(--surface); color: var(--text-1);
      border: 1px solid var(--border); border-radius: var(--r-md); box-shadow: var(--shadow-md); font-weight: 600; }


    .wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 24px; }

    /* ===== NAV =====
       Class-scoped, not `nav {}`: the footer link block is a <nav> too now, and
       a bare element selector would have pinned it to the top of the viewport. */
    .topbar {
      position: fixed; top: 0; left: 0; right: 0; z-index: 100;
      background: var(--glass-bg);
      -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
      border-bottom: 1px solid var(--border);
      height: 62px;
    }
    .topbar .wrap { display: flex; align-items: center; justify-content: space-between; height: 100%; }
    .nav-logo { display: flex; align-items: center; gap: 11px; font-weight: 700; font-size: 1.2rem; letter-spacing: -0.02em; color: var(--text-1);
      flex-shrink: 0; min-height: 44px; margin-right: 12px; }
    .nav-logo svg { width: 34px; height: 34px; filter: drop-shadow(0 3px 8px var(--accent-glow)); }
    /* Fortschrittslinie unter der Topbar: der billigste ehrliche Weg, "wo bin
       ich" auf einer Seite von 15 Bildschirmen zu beantworten. Sie sitzt IN der
       Leiste, verbraucht also keine eigene Hoehe. */
    .nav-progress { position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: transparent; }
    .nav-progress span { display: block; height: 100%; width: 0%; background: var(--accent); transition: width .1s linear; }
    @media (prefers-reduced-motion: reduce) { .nav-progress span { transition: none; } }

    /* Sprungmenue. <details> statt eigenem Widget: das Oeffnen, Schliessen und
       die Escape-Taste kommen vom Browser, und ohne JS bleibt es bedienbar. */
    .nav-jump { position: relative; margin-left: auto; margin-right: 14px; }
    .nav-jump summary { display: inline-flex; align-items: center; gap: 7px; min-height: 44px; padding: 0 12px;
      border-radius: var(--r-sm); border: 1px solid var(--border); background: var(--surface);
      color: var(--text-2); font-size: 0.84rem; font-weight: 600; cursor: pointer; list-style: none; }
    .nav-jump summary::-webkit-details-marker { display: none; }
    .nav-jump summary:hover { border-color: var(--border-strong); color: var(--text-1); }
    .nav-jump summary svg { width: 15px; height: 15px; }
    .nav-jump ul { position: absolute; top: calc(100% + 8px); left: 0; z-index: 60; min-width: 232px;
      list-style: none; margin: 0; padding: 7px; border-radius: var(--r-md);
      background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow-lg); }
    .nav-jump a { display: block; padding: 9px 12px; border-radius: var(--r-sm); min-height: 44px;
      display: flex; align-items: center; color: var(--text-2); font-size: 0.88rem; font-weight: 500; text-decoration: none; }
    .nav-jump a:hover, .nav-jump a:focus-visible { background: var(--bg-alt); color: var(--text-1); }
    /* Unter 700px traegt die Leiste schon Logo, Sprache, Theme und GitHub. Das
       Menue faellt dort weg - die Sticky-Pille am Daumen ist der mobile Weg zum
       Ziel, und eine sechste Schaltflaeche in einer 390px-Leiste ist keine. */
    /* Auf dem Telefon bleibt das Sprungmenue, verliert aber sein Label: die
       Seite ist dort 23,9 Bildschirme lang, und die Fortschrittslinie beantwortet
       "wo bin ich", nicht "was kommt noch". Vier Laeufe lang war es hier ganz
       ausgeblendet - also fehlte es genau auf der Fassung, die es am noetigsten
       hat. Platz ist da, weil `.nav-install` unter 700px ohnehin der Pille
       weicht. Der barrierefreie Name steht am `summary` (data-t-aria). */
    @media (max-width: 700px) {
      .nav-jump { margin-right: 8px; }
      /* Ohne Label faellt das `summary` auf die Breite seines Icons zusammen -
         gemessen 37x40, also unter der Zielgroesse. Die Hoehe steht hier
         ausdruecklich neben der Breite, weil `min-height: 40px` der Basisregel
         sonst gewinnt. */
      .nav-jump summary { padding: 0; gap: 0; min-width: 44px; min-height: 44px; justify-content: center; }
      .nav-jump summary span { display: none; }
      .nav-jump ul { left: auto; right: 0; }
    }
    .nav-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
    /* The star count made the GitHub pill wrap to two lines and it then set the
       height of the whole bar; below 400px the wordmark yields to the icon so the
       logo stops touching the language button. */
    .nav-gh { white-space: nowrap; }
    @media (max-width: 400px) {
      .topbar .nav-logo { font-size: 0; gap: 0; margin-right: 8px; min-width: 44px; }
    }
    .nav-btn {
      background: none; border: 1px solid var(--border); color: var(--text-2);
      cursor: pointer; padding: 8px 12px; border-radius: var(--r-sm); font-size: 0.82rem; font-weight: 600;
      font-family: var(--ff); display: flex; align-items: center; justify-content: center; gap: 6px;
      min-height: 44px; min-width: 44px; /* WCAG touch target (both axes) */
      transition: background .18s, color .18s, border-color .18s, transform .18s var(--ease-out);
    }
    .nav-install { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); box-shadow: 0 3px 12px var(--accent-glow); }
    .nav-install:hover { background: var(--accent-hover); color: var(--accent-ink); border-color: var(--accent-hover); }
    .nav-btn:hover { background: var(--surface); color: var(--text-1); border-color: var(--border-strong); }
    .nav-btn:active { transform: scale(0.95); }
    .nav-btn svg { width: 16px; height: 16px; }
    .nav-gh { background: var(--text-1); color: var(--bg); border-color: var(--text-1); }
    .nav-gh:hover { background: var(--text-1); color: var(--bg); opacity: 0.86; }
    #gh-stars-nav:not(:empty) { font-variant-numeric: tabular-nums; }

    /* ===== Buttons ===== */
    .btn {
      display: inline-flex; align-items: center; gap: 9px; font-family: var(--ff);
      font-weight: 600; font-size: 1rem; cursor: pointer; border: 1px solid transparent;
      padding: 14px 26px; border-radius: var(--r-md); transition: transform .2s var(--ease-out), box-shadow .2s, background .2s, border-color .2s, color .2s;
      white-space: nowrap;
    }
    .btn svg { width: 18px; height: 18px; }
    .btn-primary { background: var(--accent); color: var(--accent-ink); box-shadow: 0 4px 16px var(--accent-glow); }
    .btn-primary:hover { background: var(--accent-hover); transform: translateY(-2px); box-shadow: 0 10px 28px var(--accent-glow); color: var(--accent-ink); }
    .btn-primary:active { transform: translateY(0) scale(0.98); }
    .btn-secondary { background: var(--surface); color: var(--text-1); border-color: var(--border); box-shadow: var(--shadow-sm); }
    .btn-secondary:hover { border-color: var(--border-strong); transform: translateY(-2px); box-shadow: var(--shadow-md); }
    .btn-secondary:active { transform: translateY(0) scale(0.98); }

    /* ===== Section primitives ===== */
    section { position: relative; }
    .eyebrow {
      display: inline-flex; align-items: center; gap: 8px; font-size: 0.78rem; font-weight: 700;
      letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent);
    }
    .eyebrow::before { content: ''; width: 22px; height: 1.5px; background: var(--accent); opacity: 0.6; }
    .sec-head { max-width: 640px; }
    .sec-head.center { margin: 0 auto; text-align: center; }
    .sec-head.center .eyebrow::before { display: none; }
    .sec-title {
      font-size: clamp(1.9rem, 4vw, 3rem); font-weight: 700; line-height: 1.08;
      letter-spacing: -0.035em; margin: 16px 0 14px; text-wrap: balance;
    }
    .sec-title em { font-style: italic; font-weight: 600; color: var(--accent); }
    .sec-desc { font-size: 1.075rem; color: var(--text-2); line-height: 1.65; max-width: 56ch; }
    .sec-head.center .sec-desc { margin-left: auto; margin-right: auto; }
    /* Four heads sit flush left under an eyebrow - see it (gallery), this is
       what it replaces (swap), it fits together (handoffs), it outlives us
       (longevity). Those are the page's chapter marks; the centred heads
       between them belong to the chapter above. The distinction only reads if
       the chapters are visibly louder, so all four take a step up together.
       Eleven headlines at one size is a list, not a hierarchy - and picking one
       to enlarge just moved the question to which one deserved it.

       Es waren drei, bis .swap dazukam. Vier von acht Sektionen sind damit
       Kapitelmarken, und weiter darf das nicht wachsen: ab der Haelfte ist die
       Unterscheidung wieder eine Liste. */
    .sec-head.lead { max-width: 780px; }
    .sec-head.lead .sec-title { font-size: clamp(2.3rem, 5.2vw, 3.9rem); }

    /* ===== HERO ===== */
    .hero { padding: 150px 0 72px; position: relative; overflow: hidden; }
    .hero-glow {
      position: absolute; top: -240px; left: 50%; transform: translateX(-50%);
      width: 1100px; height: 900px; pointer-events: none; z-index: 0;
      background: radial-gradient(50% 50% at 50% 40%, var(--accent-glow) 0%, transparent 70%);
    }
    .hero-glow::after {
      content: ''; position: absolute; top: 120px; right: 120px; width: 360px; height: 360px;
      background: radial-gradient(circle, color-mix(in srgb, var(--m-meals) 18%, transparent) 0%, transparent 70%);
      filter: blur(20px);
    }
    .hero .wrap { position: relative; z-index: 1; display: grid; grid-template-columns: 1.05fr 0.95fr; gap: 56px; align-items: center; }
    .hero-badge {
      display: inline-flex; align-items: center; gap: 8px; padding: 7px 14px 7px 12px; border-radius: var(--r-full);
      font-size: 0.8rem; font-weight: 600; color: var(--text-2);
      background: var(--glass-bg); -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
      border: 1px solid var(--border); box-shadow: var(--shadow-sm); margin-bottom: 26px;
    }
    .hero-badge .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--m-tasks); box-shadow: 0 0 0 3px color-mix(in srgb, var(--m-tasks) 22%, transparent); }
    .hero h1 {
      font-size: clamp(2.7rem, 6vw, 4.4rem); font-weight: 700; line-height: 1.03;
      letter-spacing: -0.04em; margin-bottom: 22px; text-wrap: balance;
    }
    .hero h1 em { font-style: italic; font-weight: 600; color: var(--accent); }
    .hero-sub { font-size: 1.18rem; color: var(--text-2); max-width: 30ch; margin-bottom: 34px; line-height: 1.6; }
    .hero-actions { display: flex; gap: 12px; flex-wrap: wrap; }
    .hero-tags { display: flex; gap: 8px; margin-top: 26px; flex-wrap: wrap; }
    .tag {
      padding: 5px 13px; border-radius: var(--r-full); font-size: 0.74rem; font-weight: 600;
      background: var(--surface); color: var(--text-2); border: 1px solid var(--border);
    }

    /* Hero visual */
    .hero-visual { position: relative; perspective: 1600px; }
    .hero-frame {
      border-radius: var(--r-xl); overflow: hidden; border: 1px solid var(--border);
      box-shadow: var(--shadow-hero);
      transform: rotateY(-6deg) rotateX(3deg) translateZ(0); transform-origin: center;
      transition: transform .6s var(--ease-out);
    }
    .hero-frame:hover { transform: rotateY(0) rotateX(0); }
    .hero-frame img { width: 100%; display: block; }
    .hero-float {
      position: absolute; bottom: -28px; right: -22px; width: 150px;
      border-radius: var(--r-phone); border: 4px solid var(--surface); box-shadow: var(--shadow-xl);
      z-index: 2; overflow: hidden; background: var(--surface);
    }
    .hero-float img { width: 100%; display: block; aspect-ratio: 1320 / 2867; }

    /* ===== Proof bar ===== */
    .proof { border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); background: var(--bg-alt); }
    .proof .wrap { display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
      gap: 10px 18px; padding: 16px 24px; font-size: 0.84rem; font-weight: 600; color: var(--text-2); }
    .proof b { color: var(--text-1); font-variant-numeric: tabular-nums; }
    /* Decorative, but still a text node a screen reader can reach and a sighted
       user squints at: lift it to the same tone as the surrounding labels. */
    .proof .sep { color: var(--text-3); }
    /* Die staerkste Behauptung der Seite verlinkte drei Laeufe lang auf nichts. */
    .proof-link { color: inherit; text-decoration: underline; text-underline-offset: 3px;
      text-decoration-color: var(--border-strong); min-height: 44px; display: inline-flex; align-items: center; gap: 4px; }
    .proof-link:hover { text-decoration-color: var(--accent); }
    /* Was unter 560px faellt, war frueher "18 Module" und "24 Sprachen" - also
       genau die zwei Zahlen, die die Positionierung tragen, waehrend die
       Versionsnummer stehen blieb. Jetzt umgekehrt: die Leiste darf umbrechen,
       und die Version ist das Entbehrliche. */
    @media (max-width: 560px) { .proof .ver { display: none; } }

    /* ===== Gallery =====
       Position 2 on purpose. The screenshots used to render at 544px inside the
       feature rows - a fifth of their source width, at which the app's 15px UI
       text collapses to about 3px. They were decoration, not evidence. Here each
       one gets the full content column (>= 900px at every desktop width), the
       caption does the selling, and the first readable screen lands inside the
       first 1500px instead of at 71% scroll depth. */
    .gallery { padding: 88px 0; }
    .gal-list { display: flex; flex-direction: column; gap: 56px; margin-top: 44px; }
    .gal-item { position: relative; }
    .gal-cap { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; margin-bottom: 14px; }
    .gal-kicker {
      display: inline-flex; align-items: center; gap: 7px; flex-shrink: 0;
      font-size: 0.78rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
      color: var(--m); padding: 5px 11px; border-radius: var(--r-full);
      background: color-mix(in srgb, var(--m) 12%, transparent);
    }
    .gal-cap p { font-size: 1.02rem; color: var(--text-2); line-height: 1.5; margin: 0; }
    .gal-cap b { color: var(--text-1); font-weight: 600; }
    .gal-frame { position: relative; border-radius: var(--r-xl); }
    /* Same radial accent the feature rows use, so the module colour still reads
       as the app's vocabulary and not as a second decoration system. */
    .gal-frame::before {
      content: ''; position: absolute; inset: -8% 4%; z-index: -1; border-radius: 50%;
      background: radial-gradient(closest-side, color-mix(in srgb, var(--m) 22%, transparent), transparent 72%);
      filter: blur(46px); opacity: 0.75;
    }
    .gal-frame img {
      width: 100%; aspect-ratio: 4 / 3; border-radius: var(--r-xl);
      border: 1px solid var(--border); box-shadow: var(--shadow-xl);
    }

    /* ===== What it replaces =====
       A ledger, not a card grid: two columns per row, the left one muted and the
       right one carrying the module. Deliberately no per-row module accent - the
       accent system already carries meaning in .handoff, and a second coloured
       list turns that vocabulary back into decoration. */
    .swap { padding: 96px 0; }
    .swap-list { margin-top: 44px; border-top: 1px solid var(--border); }
    .swap-row { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.25fr); align-items: center; gap: 20px;
      padding: 17px 4px; border-bottom: 1px solid var(--border); }
    /* Rechtsbuendig, damit der Pfeil an dem steht, worauf er zeigt. Linksbuendig
       standen zwischen Label und Pfeil gemessen 223 bis 360px Leerraum, und der
       Pfeil las sich als Zubehoer der rechten Spalte statt als Verbindung. */
    .swap-from { font-size: 1rem; color: var(--text-3); text-align: right; }
    .swap-arrow { width: 17px; height: 17px; color: var(--text-3); opacity: 0.5; flex-shrink: 0; }
    .swap-to { font-size: 1rem; color: var(--text-2); line-height: 1.5; }
    /* Der Pfeil ist aria-hidden, die Beziehung stand also nur im Bild. */
    .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
      overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
    .swap-to b { color: var(--text-1); font-weight: 700; }
    .swap-close { text-align: center; margin-top: 44px; font-size: clamp(1.3rem, 3vw, 1.7rem); font-weight: 600;
      letter-spacing: -0.02em; font-style: italic; color: var(--text-1); }
    .swap-close span { color: var(--accent); }
    /* Same break as .ho-flow, and deliberately the same one: both are an "A leads
       to B" row, so they must not fold differently. Three columns at 390px would
       leave 110px for the left label. */
    @media (max-width: 700px) {
      .swap-row { grid-template-columns: 1fr; gap: 5px; padding: 13px 4px; }
      .swap-from, .swap-to { font-size: 0.94rem; }
      .swap-arrow { transform: rotate(90deg); margin-left: 2px; width: 15px; height: 15px; }
    }

    /* ===== Feature showcase (alternating) ===== */
    /* ===== Handoffs =====
       Deliberately not a card grid: each row is a directed sequence, and the
       direction carries the meaning. The two module accents per row are the
       app's own colour vocabulary used literally - the module that produces the
       data on the left, the one that consumes it on the right. */
    .handoff { padding: 96px 0 88px; }
    /* Flush left, not centred: the head above it is a left-aligned chapter mark,
       and a centred 900px list under a 0-offset head reads as a 126px misprint. */
    .ho-list { list-style: none; margin: 56px 0 0; max-width: 900px; display: grid; gap: 0; }
    .ho-row { padding: 30px 0; border-top: 1px solid var(--border); }
    .ho-row:last-child { border-bottom: 1px solid var(--border); }
    .ho-flow { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
    .ho-step { display: inline-flex; align-items: center; gap: 12px; min-width: 0; }
    .ho-ico { width: 42px; height: 42px; flex-shrink: 0; border-radius: var(--r-md); display: grid; place-items: center;
      color: var(--m); background: color-mix(in srgb, var(--m) 12%, transparent);
      border: 1px solid color-mix(in srgb, var(--m) 26%, transparent); }
    .ho-ico svg { width: 21px; height: 21px; }
    .ho-label { font-size: 1.05rem; font-weight: 650; letter-spacing: -0.015em;
      color: color-mix(in srgb, var(--m) 84%, var(--text-1)); text-wrap: balance; }
    .ho-arrow { color: var(--text-3, var(--text-2)); flex-shrink: 0; opacity: 0.55; }
    .ho-arrow svg { width: 26px; height: 26px; display: block; }
    .ho-note { margin-top: 14px; margin-left: 54px; font-size: 0.97rem; color: var(--text-2); line-height: 1.6; max-width: 62ch; }
    @media (max-width: 700px) {
      .ho-flow { flex-direction: column; align-items: flex-start; gap: 10px; }
      .ho-arrow { transform: rotate(90deg); margin-left: 8px; }
      .ho-label { font-size: 1rem; }
      .ho-note { margin-left: 0; }
    }

    /* Eine Sektion fuer beide Stufen. Der `--bg-alt`-Streifen sass frueher an
       der zweiten Sektion und trennte die vier ausfuehrlichen Module von den
       vierzehn Karten, obwohl beide dasselbe sagen. */
    .showcase { padding: 96px 0; background: var(--bg-alt); border-block: 1px solid var(--border); }
    /* 2x2 statt vier Zeilen ueber die volle Breite. Die Zeilen waren je 405px
       hoch, weil ein Hochkant-Handy die Hoehe trieb; im Raster teilen sich zwei
       Karten eine Reihe und die Hoehe faellt auf das Mass des Textes daneben.
       Gemessen 1.908px -> rund 640px. */
    .feat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 26px; margin-top: 48px; }
    .feat-row { display: grid; grid-template-columns: minmax(0, 1fr) 200px; gap: 24px; align-items: center;
      padding: 26px; border-radius: var(--r-lg); background: var(--surface);
      border: 1px solid var(--border); box-shadow: var(--shadow-sm); }
    .feat-row:last-child { margin-bottom: 0; }
    /* The kicker keeps its module accent - that is the one place on this page
       where the app's colour vocabulary is semantically correct. But at 12.5px
       bold the raw tokens missed AA on the lightest two (--m-shopping 4.40:1,
       --m-split 4.48:1), so the label darkens the hue slightly while the icon
       plate, which is not text, keeps the pure accent. */
    .feat-kicker { display: inline-flex; align-items: center; gap: 8px; font-size: 0.78rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
      color: color-mix(in srgb, var(--m) 84%, var(--text-1)); margin-bottom: 14px; }
    .feat-kicker .k-ico { width: 26px; height: 26px; border-radius: var(--r-sm); display: grid; place-items: center;
      color: var(--m); background: color-mix(in srgb, var(--m) 13%, transparent); }
    .feat-kicker .k-ico svg { width: 15px; height: 15px; }
    .feat-info h3 { font-size: clamp(1.15rem, 1.7vw, 1.4rem); font-weight: 700; letter-spacing: -0.03em; line-height: 1.12; margin-bottom: 14px; text-wrap: balance; }
    .feat-info p { color: var(--text-2); font-size: 0.95rem; line-height: 1.6; }
    .feat-list { list-style: none; margin-top: 20px; display: flex; flex-direction: column; gap: 10px; }
    .feat-list li { display: flex; align-items: flex-start; gap: 9px; font-size: 0.88rem; color: var(--text-2); }
    .feat-list svg { width: 18px; height: 18px; color: var(--m); flex-shrink: 0; margin-top: 2px; }
    .feat-visual { position: relative; }
    /* Die Feature-Spalte ist bei 1440px 544px breit. Eine Desktopaufnahme aus einer
       2752px-Quelle landet darin 5,1-fach verkleinert, der 15px-UI-Text bei 2,9px -
       der Quelltextkommentar dieser Datei nannte genau das "decoration, not
       evidence" und baute deswegen die Galerie, waehrend es hier stehen blieb.
       Die Handyaufnahme ist 480px breit und laeuft bei 300px auf das 1,6-fache;
       sie ist der einzige Beleg, der in dieser Spalte lesbar ist. Die
       Desktop-Ansichten tragen jetzt ausschliesslich die Galerie. */
    /* Der Glow sass auf negativen Insets und ragte damit zwischen 460px und 860px
       Viewport bis zu 25px ueber das Dokument hinaus (von `body { overflow-x:
       hidden }` flach abgeschnitten, also sichtbar als gekappte Kante statt als
       Scrollleiste). Er haengt jetzt am Bild statt am Container und bleibt drin. */
    .feat-visual::before {
      content: ''; position: absolute; inset: 6% 12% 6% 12%; z-index: -1; border-radius: var(--r-2xl);
      background: radial-gradient(60% 60% at 50% 40%, color-mix(in srgb, var(--m) 18%, transparent), transparent 75%);
      filter: blur(26px);
    }
    .feat-phone { display: block; width: 100%; aspect-ratio: 1320 / 1780;
      object-fit: cover; object-position: top center;
      border-radius: var(--r-phone) !important; border: 4px solid var(--surface);
      box-shadow: var(--shadow-lg); position: relative; z-index: 2; }

    /* ===== Feature grid ===== */
    /* Handyaufnahmen der Module, die sonst nur Prosa waeren. Feste Breite statt
       fr-Spalten: die Reihe soll umbrechen statt die Bilder zu stauchen, und ein
       Telefon unter ~150px zeigt nichts mehr. */
    .mod-shots { display: flex; flex-wrap: wrap; justify-content: center; gap: 26px 22px; margin-top: 52px; }
    .mod-shot { width: 176px; }
    .mod-shot img { width: 100%; aspect-ratio: 1320 / 1900; object-fit: cover; object-position: top center;
      border-radius: var(--r-phone);
      border: 4px solid var(--surface); box-shadow: var(--shadow-lg); display: block; }
    .mod-shot figcaption { margin-top: 12px; text-align: center; font-size: 0.82rem; font-weight: 600; color: var(--text-3); }
    /* 176px breite Telefone brechen bei 390px auf drei Reihen um und kosten dort
       1.539px - mehr als das Karussell, das diese Reihe ersetzt hat. Bei 104px
       passen drei nebeneinander, es bleiben zwei Reihen. Auf Daumennagelgroesse
       ist das der Beleg, dass es das Modul gibt, und mehr soll es hier nicht sein.
       Steht bewusst HINTER der Basisregel: gleiche Spezifitaet, es entscheidet
       die Quellreihenfolge. */
    @media (max-width: 700px) {
      .mod-shots { gap: 20px 14px; margin-top: 40px; }
      .mod-shot { width: 104px; }
      .mod-shot figcaption { margin-top: 8px; font-size: 0.74rem; }
    }
    .mod-lead { max-width: 62ch; margin: 72px auto 0; text-align: center; font-size: 1.02rem;
      line-height: 1.65; color: var(--text-2); }
    .mod-scope { max-width: 68ch; margin: 44px auto 0; padding-top: 30px; border-top: 1px solid var(--border);
      font-size: 0.98rem; line-height: 1.7; color: var(--text-2); text-align: center; }
    /* Vierzehn Karten gehen in keiner brauchbaren Spaltenzahl auf (14 teilt sich
       nur durch 1, 2, 7 und 14), die letzte Reihe bleibt also angebrochen: 5+5+4
       bei 1440px, 4+4+4+2 bei 1152px. Das per `grid-column: span` zu fuellen
       haengt an einer Spaltenzahl, die `auto-fill` je nach Breite anders waehlt -
       es braeche bei drei Spalten. Eine angebrochene Schlusszeile in einem
       Kartenraster ist gewoehnlich; die fragile Regel dagegen nicht. */
    .mod-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(255px, 1fr)); gap: 16px; margin-top: 26px; }
    .mod-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg); padding: 26px; box-shadow: var(--shadow-sm);
      transition: transform .25s var(--ease-out), box-shadow .25s, border-color .25s; position: relative; overflow: hidden; }
    .mod-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--m); opacity: 0; transition: opacity .25s; }
    .mod-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); border-color: color-mix(in srgb, var(--m) 30%, var(--border)); }
    .mod-card:hover::before { opacity: 1; }
    .mod-ico { width: 46px; height: 46px; border-radius: var(--r-md); display: grid; place-items: center; margin-bottom: 16px;
      color: var(--m); background: color-mix(in srgb, var(--m) 13%, transparent); }
    .mod-ico svg { width: 23px; height: 23px; }
    .mod-card h3 { font-size: 1.02rem; font-weight: 700; letter-spacing: -0.01em; margin-bottom: 7px; }
    .mod-card p { font-size: 0.875rem; color: var(--text-2); line-height: 1.6; }


    /* ===== Platform / Install anywhere ===== */
    .platforms { padding: 100px 0; }
        /* 190px liessen bei 1.152px nur fuenf Karten in die Reihe, die sechste stand
       allein darunter. 168px bringen alle sechs unter, auch bei 1.152px:
       6x168 + 5x16 = 1.088. */
    .plat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); gap: 16px; margin-top: 48px; }
    .plat-card {
      position: relative; display: flex; flex-direction: column; align-items: flex-start; gap: 14px;
      background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg); padding: 24px;
      box-shadow: var(--shadow-sm); transition: transform .25s var(--ease-out), box-shadow .25s, border-color .25s; color: inherit;
    }
    .plat-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); border-color: var(--border-strong); }
    .plat-logo { width: 44px; height: 44px; display: grid; place-items: center; border-radius: var(--r-md);
      background: color-mix(in srgb, var(--pc, var(--accent)) 12%, transparent); color: var(--pc, var(--accent)); }
    .plat-logo svg { width: 26px; height: 26px; fill: currentColor; }
    .plat-card h3 { font-size: 1.02rem; font-weight: 700; letter-spacing: -0.01em; }
    .plat-card p { font-size: 0.85rem; color: var(--text-2); line-height: 1.55; }
    .plat-card code { font-family: var(--ff-mono); font-size: 0.8em; background: var(--accent-soft); color: var(--accent-deep); padding: 1px 5px; border-radius: var(--r-xs); }
    /* "1-click" is a status, not a module: it was wearing --m-tasks green, which
       teaches the wrong vocabulary and landed at 4.21:1. Semantic success token,
       darkened enough to clear AA at this size. */
    .plat-badge { position: absolute; top: 16px; right: 16px; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.04em;
      text-transform: uppercase; color: var(--success-ink); background: color-mix(in srgb, var(--success-ink) 13%, transparent);
      padding: 4px 9px; border-radius: var(--r-full); }
    /* Six cards, three of them wearing a green "1-click" badge that reads as the
       recommendation - while the command block directly below belongs to one of
       the three without a badge. One card now says which one that is. Same
       hue-darkening as .feat-kicker: the raw accent misses AA on the tint. */
    .plat-badge--start { color: color-mix(in srgb, var(--accent) 84%, var(--text-1));
      background: color-mix(in srgb, var(--accent) 13%, transparent); }
    /* Label uses the theme accent (AA-contrast on --surface in both themes); the
       platform brand colour (--pc) stays on the logo tint only. */
    .plat-go { margin-top: auto; display: inline-flex; align-items: center; gap: 6px; font-size: 0.84rem; font-weight: 700; color: var(--accent); }
    .plat-go svg { width: 15px; height: 15px; transition: transform .2s var(--ease-out); }
    .plat-card:hover .plat-go svg { transform: translateX(4px); }

    /* Die 760px-Deckelung liess in einer 1.152px-Spalte 440px totes Gutter
       rechts vom wichtigsten Block der Seite stehen, waehrend das Plattformraster
       darueber die volle Breite nutzte. Drei Laeufe lang gemeldet. Die Trust-Bar
       darin geht damit von 3+2 auf 5 Spalten, also eine Zeile. */
    .quick-docker { margin-top: 44px; }
    .code-block { background: var(--code-bg); color: var(--code-text); border-radius: var(--r-lg); padding: 22px 26px;
      font-family: var(--ff-mono); font-size: 0.84rem; line-height: 1.9; overflow-x: auto; box-shadow: var(--shadow-lg); border: 1px solid rgba(255,255,255,0.06);
      white-space: pre; tab-size: 2; }
    .code-block .c { color: var(--code-comment); }
    .code-block .h { color: var(--code-accent); }
    /* Copy button, mirrored from install.html - the landing page asked people to
       select four shell lines by hand while the install page had twelve of these. */
    .code-wrap { position: relative; }
    /* The button gets its own band at the top of the plate instead of a
       padding-right gutter. `padding-right` only protects the first screenful:
       the button is absolutely positioned, so any line longer than the plate
       scrolls straight underneath it. Measured on the German page, where the
       label reads "Kopieren" at 94px against a 62px gutter, that hid 17% of the
       readable width at 390px and 23% at 320px, on every block. A reserved row
       costs 32px of height and cannot be scrolled under. */
    .code-wrap:has(> .copy-btn) .code-block { padding-top: 54px; }
    .copy-btn { position: absolute; top: 10px; right: 10px; display: inline-flex; align-items: center; justify-content: center; gap: 5px;
      background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.14); color: var(--code-text); cursor: pointer;
      padding: 7px 11px; min-height: 44px; border-radius: var(--r-sm); font-family: var(--ff); font-size: 0.75rem; font-weight: 600; transition: all .15s; }
    .copy-btn:hover { background: rgba(255,255,255,0.18); color: #fff; }
    /* Zwei gefuellte Zustands-Chips, bewusst themenunabhaengig: sie erscheinen
       fuer zwei Sekunden nach einem Klick, und weisse Tinte auf diesen Tiefen
       traegt auf jedem Grund (5.33:1 / 4.98:1). Die Werte sind --color-success
       und --color-warning der App; hier standen die der abgeloesten Palette. */
    .copy-btn.copied { background: #1E7B35; border-color: #1E7B35; color: #fff; }
    .copy-btn.failed { background: #A85D00; border-color: #A85D00; color: #fff; }
    .copy-btn svg { width: 13px; height: 13px; }
    /* What the reader is actually deciding at the moment they paste a curl into
       the machine that will hold their family's health records: what runs, how
       big it is, what it opens, and whether it talks to anyone. All of it lived
       elsewhere - the footprint on the other page, "0 trackers" 12.000px away. */
    /* Above the command, not below it. It sat underneath, so the eye reached the
       copy button and `docker compose up -d` first and the answer to "what does
       this do to my machine" arrived after the decision was already made. */
    .trust-bar { list-style: none; display: grid; grid-template-columns: repeat(auto-fit, minmax(196px, 1fr));
      gap: 10px 24px; margin-bottom: 20px; padding-bottom: 18px; border-bottom: 1px solid var(--border); }
    .trust-bar li { font-size: 0.86rem; color: var(--text-2); line-height: 1.5; display: flex; gap: 8px; }
    .trust-bar b { color: var(--text-1); font-weight: 600; flex-shrink: 0; }
        /* `anywhere` brach den Image-Tag mitten im Namen ("ghcr.io/ulsklyc/yuvo" /
       "mi:latest") - drei Laeufe lang gemeldet. `break-word` bricht erst, wenn
       ein Wort wirklich nicht passt, und das <wbr> im Markup gibt ihm vorher
       eine sinnvolle Stelle: hinter dem letzten Schraegstrich. */
    .trust-bar code { font-family: var(--ff-mono); font-size: 0.94em; color: var(--text-1); overflow-wrap: break-word; }
    /* The one irreversible thing on this page. The block above generates two
       secrets and starts the container; nothing on this page used to say that
       one of the two can never be regenerated, while install.html says it twice. */
    .quick-warn { display: flex; gap: 11px; margin-top: 14px; padding: 13px 16px; border-radius: var(--r-md);
      background: color-mix(in srgb, var(--warn-ink) 10%, transparent);
      border: 1px solid color-mix(in srgb, var(--warn-ink) 32%, transparent);
      font-size: 0.88rem; line-height: 1.55; color: var(--text-1); }
    .quick-warn svg { width: 17px; height: 17px; flex-shrink: 0; margin-top: 2px; color: var(--warn-ink); }
    .quick-warn b { font-weight: 700; }
    .quick-warn code { font-family: var(--ff-mono); font-size: 0.92em; overflow-wrap: anywhere; }
    /* The start command lives in its own block BELOW the warning, so the .env edit
       cannot be skipped by pasting one long chain. Same gap the warning takes from
       the block above it, so the three parts read as one sequence. */
    .quick-warn + .code-wrap { margin-top: 14px; }
    /* Fielen beim Entfernen der Setup-Sektion mit weg, das <p> blieb stehen: der
       Satz rutschte damit auf 16px/--text-1 ohne Abstand und las sich als
       Fortsetzung der Codeplatte, waehrend die <code>-Chips daneben ihre
       Toenung behielten. Dieselbe Auszeichnung, zwei Erscheinungsbilder. */
    .quick-note { font-size: 0.9rem; color: var(--text-2); margin-top: 16px; line-height: 1.6; }
    .quick-note code { font-family: var(--ff-mono); font-size: 0.82rem; background: var(--accent-soft); color: var(--accent-deep); padding: 2px 7px; border-radius: var(--r-xs); }
    /* The one sentence that answers the fear this audience actually has was a
       0.92rem caption in 92% white, under the CTA buttons, at 94% scroll depth.
       It gets a chapter of its own before the install block rather than after
       it - the reader decides here, not below.

       Seit dem Umbau sind es drei Fragen mit drei Antworten in Karten, nicht
       mehr ein Absatz in Fliesstextgroesse. Das Padding stand noch auf
       `30px 0 8px` aus der Absatz-Fassung und gab der Sektion, die die Seite
       selbst als Entscheidungspunkt fuehrt, acht Pixel Luft nach unten. */
    .longevity { padding: 96px 0; }
    .long-list { margin-top: 44px; display: grid; gap: 20px; }
    .long-item { padding: 26px 28px; border-radius: var(--r-lg); background: var(--surface);
      border: 1px solid var(--border); box-shadow: var(--shadow-sm); }
    .long-item dt { font-size: 1.08rem; font-weight: 700; letter-spacing: -0.01em; margin-bottom: 9px; color: var(--text-1); }
    .long-item dd { margin: 0; font-size: 1rem; color: var(--text-2); line-height: 1.65; }
    .long-item dd b { color: var(--text-1); font-weight: 650; }


    /* ===== CTA ===== */
    .cta { padding: 110px 0; }
    /* Was a full-bleed violet gradient covering 40% of the viewport (50% on a
       phone) on a page whose design system caps violet at 10%. Drenching the
       plate also forced its own text and button palette - white ink, a white
       primary button, a ghost secondary - none of which existed anywhere else.
       The frame carries the accent now, so the single violet fill left in the
       section is the button the section exists to get pressed. */
    /* Second pass: undrenching the gradient left the block with no weight at all
       - 1.0% accent in the conversion viewport, a plate at 1.05:1 against the
         page, a hairline frame at 2.21:1 against its own fill carrying the whole
         identity, and an h2 that at 43.2px was the SMALLEST on the page above
         1080px. The fill carries the accent now (a tint, which DESIGN.md
         sanctions for "accent highlight zones"), the frame only outlines it, and
         the headline outranks the centred section heads it is supposed to close
         (51.2px gegen 48px). Gegen die vier Kapitelmarken bleibt sie bewusst
         darunter (62.4px): die schliessen Kapitel, der CTA schliesst die Seite,
         und zwei Groessen fuer zwei Rollen sind eine Hierarchie. */
    .cta-box { position: relative; overflow: hidden; text-align: center; border-radius: var(--r-2xl); padding: 68px 40px;
      background: color-mix(in srgb, var(--accent) 10%, var(--surface));
      border: 1px solid color-mix(in srgb, var(--accent) 80%, var(--border));
      box-shadow: var(--shadow-lg); }
    .cta-box::before { content: ''; position: absolute; inset: 0; pointer-events: none;
      background: radial-gradient(70% 100% at 50% -20%, var(--accent-glow), transparent 65%); }
    .cta-box > * { position: relative; z-index: 1; }
    .cta-box h2 { font-size: clamp(2.1rem, 4.6vw, 3.2rem); font-weight: 700; letter-spacing: -0.035em; color: var(--text-1); margin-bottom: 14px; text-wrap: balance; }
    .cta-box h2 em { font-style: italic; font-weight: 600; color: var(--accent); }
    .cta-box p { color: var(--text-2); font-size: 1.08rem; max-width: 44ch; margin: 0 auto 32px; }
    .cta-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }

    /* ===== Footer =====
       The original landing-page footer is removed in Docusaurus. Keep these
       selectors out of the global footer namespace so they cannot affect the
       shared site footer during initial stylesheet loading. */

    /* ===== Back to top =====
       The legal pages have had this since they grew a table of contents. This
       page is 16.293px tall on desktop and 22.500px on a phone - roughly five
       times either of them - and had no way back at all. */
    .to-top { position: fixed; right: 16px; bottom: calc(16px + env(safe-area-inset-bottom, 0px)); z-index: 89;
      display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 16px;
      background: var(--surface); color: var(--text-1); border: 1px solid var(--border);
      border-radius: var(--r-full); box-shadow: var(--shadow-md); font-size: 0.85rem; font-weight: 600;
      opacity: 0; visibility: hidden; transform: translateY(8px); transition: opacity .2s, transform .2s, visibility .2s; }
    .to-top.show { opacity: 1; visibility: visible; transform: none; }
    .to-top:hover { border-color: var(--border-strong); }
    .to-top svg { width: 15px; height: 15px; }

    /* ===== Mobile sticky CTA (thumb-zone Install) ===== */
    .mobile-cta { display: none; }
    /* With JS, the pill fades in only after the hero scrolls away, so it doesn't
       duplicate the in-hero Install button above the fold. No-JS keeps it visible. */
    /* visibility, not just opacity: pointer-events keeps the mouse out but an
       opacity-0 link is still in the tab order, so keyboard users hit an
       invisible "Install Yuvomi" while the hero was still on screen. .to-top on
       the legal pages already does it this way. */
    html.js .mobile-cta { opacity: 0; visibility: hidden; pointer-events: none;
      transition: opacity .3s var(--ease-out), visibility .3s var(--ease-out); }
    html.js .mobile-cta.show { opacity: 1; visibility: visible; pointer-events: auto; }

    /* ===== Reveal ===== */
    .reveal { opacity: 0; transform: translateY(26px); transition: opacity .7s var(--ease-out), transform .7s var(--ease-out); }
    .reveal.vis { opacity: 1; transform: none; }
    .d1 { transition-delay: .08s; } .d2 { transition-delay: .16s; } .d3 { transition-delay: .24s; }
    /* No-JS fallback: reveal-gated content must never ship hidden (privacy visitors often disable JS). */
    html:not(.js) .reveal { opacity: 1; transform: none; }

    /* ===== Responsive ===== */
    @media (max-width: 1024px) {
      .hero .wrap { grid-template-columns: 1fr; gap: 56px; text-align: center; }
      .hero-sub { max-width: 46ch; margin-left: auto; margin-right: auto; }
      .hero-actions, .hero-tags { justify-content: center; }
      .hero-visual { max-width: 560px; margin: 0 auto; width: 100%; }
      .hero-frame { transform: none; }
      /* c2 was hidden here but c1 kept its -30px outset. Once .hero-visual stops
         being narrower than the column it hangs past the gutter and the hero's
         own overflow:hidden shaves it - 6px off at 375px. It tucks in instead. */
    }
    @media (max-width: 860px) {
      .feat-grid { grid-template-columns: 1fr; gap: 18px; }
      /* Unter 860px hat die Karte rund 310px Inhaltsbreite. Neben einem Telefon
         blieben dem Text 162px, also zwei Woerter pro Zeile - unlesbar. Das
         Telefon faellt hier weg statt zu schrumpfen: die Galerie schaltet ab
         860px selbst auf Handyaufnahmen um und die Modulreihe zeigt fuenf
         weitere, ein mobiler Besucher sieht also weiter sieben Aufnahmen. Ein
         Bild, das den Text daneben zerstoert, belegt nichts. */
      .feat-row { grid-template-columns: 1fr; gap: 0; padding: 22px 20px; }
      .feat-row .feat-visual { display: none; }
      .feat-row.reverse .feat-visual { order: 0; }
      .gal-list { gap: 44px; }
      .gal-cap { gap: 10px; margin-bottom: 12px; }
      /* applyShots() swaps in the phone capture at this width, so the frame has
         to follow it: portrait ratio, phone-sized, and centred in the column. */
      .gal-frame img { aspect-ratio: 1320 / 2867; max-width: 300px; margin: 0 auto;
        border-radius: var(--r-phone); border-width: 4px; border-color: var(--surface); }
      .gal-frame::before { inset: -4% 14%; }
    }
    @media (max-width: 600px) {
      .hero { padding: 120px 0 56px; }
      .gallery { padding: 64px 0 16px; }
      .gal-cap p { font-size: 0.95rem; }
      .cta-box { padding: 52px 24px; }
      .nav-gh span:not(#gh-stars-nav) { display: none; }
    }
    /* Sticky Install button in the thumb zone on phones - always reachable past the hero. */
    @media (max-width: 700px) {
      .mobile-cta {
        display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; width: max-content;
        position: fixed; left: 50%; transform: translateX(-50%);
        bottom: calc(16px + env(safe-area-inset-bottom, 0px)); z-index: 90;
        padding: 14px 26px; border-radius: var(--r-full);
        background: var(--accent); color: var(--accent-ink); font-weight: 700; font-size: 0.95rem;
        box-shadow: 0 10px 30px var(--accent-glow), 0 4px 14px rgba(0,0,0,0.20);
        border: 1px solid color-mix(in srgb, var(--accent) 55%, #000 12%);
      }
      .mobile-cta svg { width: 18px; height: 18px; }
      .mobile-cta:active { transform: translateX(-50%) scale(0.97); }
      .nav-install { display: none; } /* the sticky pill replaces it on phones */
      body { padding-bottom: 84px; } /* keep footer clear of the floating pill */
      /* The pill is centred on the same bottom edge and runs to x=296 at 375px,
         so a right-aligned to-top would sit on top of it. It goes above instead,
         with enough clearance that the two boxes do not touch. */
      .to-top { bottom: calc(94px + env(safe-area-inset-bottom, 0px)); }
      /* Die alte Fassung hat geprueft, dass sich die beiden PILLEN nicht
         beruehren - und nicht, ob sie den INHALT beruehren. Sie taten es: bei
         4.200px lagen drei `.swap-row`-Zeilen darunter, also ausgerechnet die
         Sektion mit dem staerksten Argument. `body { padding-bottom }` haelt nur
         den Footer frei, nicht die 23 Bildschirme davor. Beim Abwaertslesen
         raeumen beide das Feld, beim Zurueckscrollen sind sie sofort wieder da. */
      .to-top { transition: transform .2s ease, opacity .2s ease, visibility .2s ease; }
      html.nav-down .to-top { transform: translateY(150%); opacity: 0; visibility: hidden; }
      /* Die Pille muss `html.js .mobile-cta.show` (0,3,1) schlagen, sonst bleibt
         sie sichtbar - genau das war der erste Versuch, und die Messung hat es
         gezeigt: nav-down stand, die Deckkraft blieb 1. */
      html.js.nav-down .mobile-cta.show {
        transform: translateX(-50%) translateY(150%); opacity: 0;
        visibility: hidden; pointer-events: none;
      }
    }
    @media (prefers-reduced-motion: reduce) {
      /* Animations were already fully covered; transitions were not, so the
         hover lifts on cards and buttons kept moving. Near-zero duration rather
         than `none` so transitionend still fires. */
      *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
      }
      .reveal { opacity: 1; transform: none; }
      .hero-frame { transition: none; }
      .plat-card:hover { transform: none; }
    }

    /* Print. Without this the page printed the reveal state it happened to be
       in - every section past the fold at opacity 0 - plus a fixed bar stamped
       over the first page and link targets that exist nowhere on paper. */
    @media print {
      html { scroll-behavior: auto; }
      body { background: #fff; color: #000; padding-bottom: 0; }
      body::after { content: none; }
      .skip-link, .site-bar, .copy-btn, .hero-glow, .to-top { display: none !important; }
      /* The CTA stays on paper: it used to be hidden entirely, so a printed copy
         ended on the footer with no closing line and no GitHub address - and the
         external-href rule below is what puts that address on the sheet. */
      .cta-box { box-shadow: none; padding: 28px 24px; }
      .reveal { opacity: 1 !important; transform: none !important; }
      .hero-frame { transform: none !important; box-shadow: none !important; }
      .hero, section { padding: 20px 0 !important; }
      .feat-row { margin-bottom: 32px; }
      .gal-item, .feat-row, .mod-card, .swap-row, .plat-card, .ho-row { break-inside: avoid; }
      /* Paper has no hover and no href, so external targets go inline. Internal
         anchors stay bare - "(#screens)" tells a reader nothing. */
      a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.85em; word-break: break-all; }
    }
