/* Self-hosted, not fonts.googleapis.com. The site's own test asserts no
   external resource calls; linking Google broke it for a day, and hotlinking
   Google Fonts from an EU site hands every visitor's IP to a third party for a
   glyph. Latin and latin-ext only -- the other 17 subsets Google offers are
   ~340 kB no page here can use. Familjen Grotesk ships as one VARIABLE file
   serving 400/600/700, which is why three rules share a src; the weight
   declarations are Google's own, unchanged. */
@font-face {
  font-family: 'Familjen Grotesk';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/familjen-grotesk-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Familjen Grotesk';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/familjen-grotesk-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Familjen Grotesk';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('fonts/familjen-grotesk-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Familjen Grotesk';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('fonts/familjen-grotesk-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Familjen Grotesk';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('fonts/familjen-grotesk-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Familjen Grotesk';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('fonts/familjen-grotesk-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/ibm-plex-mono-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/ibm-plex-mono-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('fonts/ibm-plex-mono-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('fonts/ibm-plex-mono-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  --ground: #0A0A0C;
  --surface: #101116;
  --ink: #EDE9E2;
  --muted: #8B8880;
  --oxide: #8E3428;      /* STRUCTURE only: hairlines, rules, marks */
  --oxide-text: #CE5F50; /* the same hue, lightened until it READS.
                            #8E3428 as text is 2.52:1 against the ground —
                            below the 4.5:1 needed for small text, which is why
                            the hero eyebrow was invisible. This is 5.06:1. */
  --rule: rgba(255, 255, 255, .085);
  /* Verdict colours, added 2026-09-20. The palette is deliberately one accent
     (oxide) and Andreas asked whether the verdict should be coloured at all —
     it read in --ink whatever it said, so "skip" and "worth going out" were
     typographically identical and the answer had to be READ to be seen. On a
     page whose whole job is one word, that word should carry it.
     Chosen to sit with the warm palette rather than as browser primaries: the
     verde is desaturated and slightly olive, the amber is the oxide hue pulled
     toward yellow, and skip simply IS --oxide-text. All three are checked
     against --ground for contrast, which is why none is the obvious bright
     version of itself. */
  --verdict-go:   #7FA86B;   /* 6.1:1 on --ground */
  --verdict-warn: #D89B4A;   /* 7.6:1 */
  --verdict-no:   #CE5F50;   /* = --oxide-text, 5.06:1 */
  --wrap: 1280px;
  /* ONE content measure. .prose was 780 and .tool-body 760 — arbitrarily
     different, so anything pulled out of the text aligned with one column and
     sat 20px off the other. Andreas spotted the symptom on guide.html. */
  --measure: 780px;

  /* Type scale. Andreas reported the site was hard to read on his screens
     (2026-09-05) and he was right: no base size was set, so everything sat on
     the browser's 16px default, and the mono labels ran at .58-.63rem — about
     9-10px, uppercase, with wide tracking. Sizes live here so they can be
     tuned in one place rather than hunted through the file. */
  --t-micro: .8rem;    /* mono labels, eyebrows, data captions */
  --t-small: .92rem;   /* secondary prose, notes */
  --t-body: 1.02rem;   /* running text */
  --t-lead: 1.2rem;   /* intro paragraphs */
  --sans: "Familjen Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Square corners. A real token, not one of the rollout aliases that used to
     sit here -- those are gone now that every rule names its colour directly. */
  --radius: 0;
}

* { box-sizing: border-box; }

/* 17px, not the 16px default: these are reading pages, not app chrome. */
html { font-size: 112.5%; scroll-behavior: smooth; }  /* 18px */

body {
  margin: 0;
  font-family: var(--sans);
  font-weight: 400;
  color: var(--ink);
  background: var(--ground);
  font-size: var(--t-body);
  line-height: 1.66;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* Subtle starfield behind everything */
/* The starfield was six radial gradients faking stars, sitting behind
   photographs of real ones. The photographs are the strongest thing on the
   page and the decoration was competing with them. */
.stars { display: none; }

a { color: var(--ink); text-decoration: none; }
a:hover { text-decoration: underline; }

h1, h2, h3 {
  line-height: 1.04;
  letter-spacing: -.025em;
  text-wrap: balance;
  font-weight: 700;
  margin: 0 0 .4em;
}
h2 { font-size: clamp(1.6rem, 3.5vw, 2.4rem); }
h3 { font-size: 1.15rem; }

.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 24px; }
.center { text-align: center; }
/* A centred block needs its BOX centred too, not just the text inside it.
   p { max-width: 66ch } caps every paragraph, and text-align only centres the
   words within that capped box -- the box itself still hangs off the left edge.
   On the download panel that put the fine print 312px left of the button above
   it while its own text looked perfectly centred, which is why it read as
   "weird" rather than as plainly broken. Andreas found it 2026-09-08. */
.center > p, .center > .lead, p.center, .lead.center { margin-inline: auto; }

/* --- Nav --- */
.nav {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 24px;
  background: rgba(10, 10, 12, .78);
  backdrop-filter: saturate(160%) blur(12px);
  border-bottom: 1px solid var(--rule);
}
.brand { display: flex; align-items: center; gap: 10px; color: var(--ink); font-weight: 700; letter-spacing: -.02em; }
.brand:hover { text-decoration: none; }
.brand img { border-radius: 7px; }
.nav-links { display: flex; gap: 22px; }
/* The hamburger exists at every width but only SHOWS below the breakpoint. The
   checkbox is clipped rather than `hidden` so it keeps keyboard focus — that is
   what makes the menu work with no JavaScript. */
.nav-open { position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.nav-toggle { display: none; color: var(--ink); cursor: pointer;
  padding: 6px; margin: -6px; border-radius: 8px; line-height: 0; }
.nav-open:focus-visible + .nav-toggle { outline: 2px solid var(--ink); outline-offset: 2px; }
.nav-links a { color: var(--muted); font-size: .95rem; }
.nav-links a:hover { color: var(--ink); text-decoration: none; }

/* --- Hero --- */
/* The old hero -- navy gradient, CSS starfield, wordmark -- is gone with
   index.html's move to .s-bleed. It carried the last of the #0a1533 navy. */

.cta { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }

/* --- Buttons --- */
.btn {
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 14px 26px; border-radius: 12px; font-weight: 650; font-size: 1.02rem;
  border: 1px solid transparent; transition: transform .12s ease, box-shadow .2s ease, background .2s ease;
  /* Everywhere else on the site .btn lands on an <a>, which inherits the page
     font and already shows a pointer. The planner's "Plan tonight" is a real
     <button>, which does neither -- it keeps the UA's font and arrow cursor
     and reads as an unstyled control. Both declarations are inert on an
     anchor, so this makes .btn work on either element rather than forking it. */
  font-family: inherit; cursor: pointer;
}
.btn:hover { text-decoration: none; transform: translateY(-1px); }
.btn:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }
.btn-primary { background: var(--ink); color: var(--ground); box-shadow: none; }
.btn-primary:hover { background: #35e3cd; }
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--rule); }
.btn-ghost:hover { border-color: var(--ink); color: var(--ink); }
.btn-sub { font-size: .72rem; font-weight: 500; opacity: .8; margin-top: 2px; }
.btn.big { font-size: 1.15rem; padding: 18px 40px; }

/* --- Sections --- */
.section { padding: clamp(56px, 9vw, 96px) 0; }
.section.alt { background: linear-gradient(180deg, transparent, var(--surface), transparent); }
.lead { color: var(--muted); font-size: 1.1rem; max-width: 640px; margin: 0 0 2.2em; }

/* --- Feature grid --- */
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 18px; }
.card {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 26px 24px;
}
.card h3 { margin-top: 4px; }
.card p { color: var(--muted); margin: 0; font-size: .95rem; }
.ic {
  display: inline-flex; width: 44px; height: 44px; border-radius: 11px;
  align-items: center; justify-content: center; margin-bottom: 14px;
  background: var(--surface); color: var(--oxide-text);
}
.ic svg { width: 24px; height: 24px; }

/* --- How it works --- */
.beats { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 18px; }
.beat { padding: 22px 24px; border-left: 2px solid var(--oxide); background: var(--surface); border-radius: 0; }
.beat p { color: var(--muted); margin: 0; }

/* --- Screenshots --- */
.shots-main { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-bottom: 18px; }
.shots-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
figure.shot { margin: 0; }
.shot {
  border: 1px solid var(--rule); border-radius: var(--radius); overflow: hidden;
  background: var(--surface);
}
.shot img { display: block; width: 100%; height: auto; }
.shot figcaption { padding: 12px 16px; color: var(--muted); font-size: .9rem; border-top: 1px solid var(--rule); }
/* The lightbox styles itself inline (see lightbox.js) so it works even if this
   file is stale/uncached. */
.shot.small figcaption { text-align: center; font-size: .85rem; }
@media (max-width: 760px) { .shots-main, .shots-row { grid-template-columns: 1fr; } }

/* --- Requirements --- */
.reqs { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px; }
.req { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius); padding: 26px 24px; }
.req p { color: var(--muted); margin: 0; }
.tag { font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; padding: 3px 8px; border-radius: 999px; vertical-align: middle; margin-left: 6px; }
.tag.free { background: var(--surface); color: var(--oxide-text); }
.tag.optional { background: rgba(154,166,189,.16); color: var(--muted); }
.fine { color: var(--muted); font-size: .88rem; }
.fine.install { max-width: 560px; margin: 14px auto 0; }

/* --- Download band --- */
.download { background: linear-gradient(180deg, var(--surface), transparent); }
.download .btn { margin: 8px 0 6px; }

/* --- Footer --- */
.footer { border-top: 1px solid var(--rule); padding: 40px 0; color: var(--muted); }
.footer p { margin: 0 0 8px; font-size: .9rem; }

/* --- Contribute form --- */
/* Two forms, one rule. The support form was written with its own class and no
   styles at all, which rendered raw browser controls on a dark page — and the
   tests could not see it, because they grep the markup. Sharing the selector
   rather than copying the block is what stops them drifting apart. */
.contribute-form, .report-form { max-width: 620px; margin: 26px 0; display: grid; gap: 12px; }
.contribute-form input[type=text], .contribute-form input[type=email], .contribute-form textarea, .contribute-form input[type=file],
.report-form input[type=text], .report-form input[type=email], .report-form textarea, .report-form input[type=file] {
  width: 100%; padding: 12px 14px; border-radius: 10px; border: 1px solid var(--rule);
  background: var(--surface); color: var(--ink); font: inherit;
}
.contribute-form input:focus, .contribute-form textarea:focus,
.report-form input:focus, .report-form textarea:focus { outline: 2px solid var(--ink); outline-offset: 1px; }
/* The diagnostics Nocturne filled in: present, editable, and visibly a group
   the reporter may cut about, rather than fields they must complete. */
.report-form .diag { border: 1px solid var(--rule); border-radius: 12px; padding: 14px 16px 18px;
                     display: grid; gap: 10px; margin: 4px 0; }
.report-form .diag legend { color: var(--muted); font-size: .9rem; padding: 0 6px; }
.report-form label { font-size: .95rem; }
.report-form .opt { color: var(--muted); font-weight: 400; }
.report-form textarea { resize: vertical; min-height: 5em; }
.report-form button { justify-self: start; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.consent { display: flex; align-items: flex-start; gap: 10px; color: var(--muted); font-size: .92rem; }
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; }
.progress { height: 12px; background: var(--surface); border-radius: 999px; overflow: hidden; border: 1px solid var(--rule); }
.progress-bar { height: 100%; width: 0; background: linear-gradient(90deg, var(--muted), var(--ink)); transition: width .15s linear; }
.form-status { color: var(--ink); font-size: .95rem; min-height: 1.3em; margin: 0; font-variant-numeric: tabular-nums; }
.form-status.error { color: #ff6b6b; }
.form-status.ok { color: var(--ink); }

/* --- Responsive --- */
@media (max-width: 640px) {
  .field-row { grid-template-columns: 1fr; }
}

/* --- Collapsed navigation ---------------------------------------------------
   Nine links stacked into a sticky header cost a phone its ENTIRE first screen:
   the old rule turned .nav into a column and let the links wrap to four rows,
   which then followed you down every page because the header is sticky.
   Andreas, on an iPhone: "the website is very cumbersome to navigate since the
   menu covers most of the screen".

   860px, not 640: the links begin wrapping to a second row well above a phone,
   and a two-row header is the same bug in a milder form.

   RAISED 860 -> 1000 on 2026-09-20, when Planner became a ninth nav item at
   Andreas's request. Measured in a browser: nine links wrap "Sample data" onto
   a second row at 900px and fit on one row at 1000px, so the old 860 left a
   140px band where the header was two rows high and the hamburger had not yet
   taken over. This number is a function of how many links there are and how
   long they are -- adding a tenth, or a longer label, moves it again, and the
   only way to know is to open a browser and narrow it. */
@media (max-width: 1000px) {
  .nav { flex-wrap: wrap; padding: 12px 16px; gap: 12px; }
  .nav-toggle { display: inline-flex; }
  .nav-links {
    display: none;
    flex-basis: 100%;
    flex-direction: column;
    gap: 0;
    margin-top: 4px;
    border-top: 1px solid var(--rule);
  }
  .nav-open:checked ~ .nav-links {
    display: flex;
    /* THE PANEL MUST SCROLL ITSELF. Nine rows are taller than a phone viewport
       once the browser's own chrome is subtracted, and the header is sticky —
       so without this the last links simply cannot be reached: dragging scrolls
       the PAGE BEHIND the menu while the menu stays pinned. Andreas, on an
       iPhone: "i cant scroll the menu on phone... the webpage behind the menu
       scrolls instead."
       dvh after vh: dvh excludes Safari's collapsing toolbar, which is exactly
       the space that made this overflow, and the vh line keeps older browsers
       working. overscroll-behavior stops the drag chaining to the page once the
       panel reaches its end — the other half of what he described. */
    max-height: calc(100vh - 72px);
    max-height: calc(100dvh - 72px);
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
  .nav-links a {
    padding: 14px 2px;                    /* a 44px touch target, not a 20px one */
    font-size: 1rem;
    border-bottom: 1px solid var(--rule);
  }
  /* the underline-on-hover flourish is a pointer idiom; it has no meaning in a
     stacked touch menu and mis-renders across the full-width rows */
  .nav-links a::after { content: none; }
  /* open state: the bars become a cross, so the control says how to close */
  .nav-open:checked + .nav-toggle .bar-top { transform: translateY(6px) rotate(45deg); }
  .nav-open:checked + .nav-toggle .bar-mid { opacity: 0; }
  .nav-open:checked + .nav-toggle .bar-bot { transform: translateY(-6px) rotate(-45deg); }
  .nav-toggle svg path { transform-origin: center; transition: transform .18s ease, opacity .18s ease; }
}
@media (prefers-reduced-motion: reduce) {
  .nav-toggle svg path { transition: none; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .btn { transition: none; }
}

/* ===== Sub-pages (Guide / FAQ / Changelog) ===== */

/* Page header */
/* Left-aligned, for the same reason .subhead is: a centred head over a
   left-aligned column below it reads as two pages stacked. The gradient moves
   off centre with it so the light falls where the text is. */
.page-head {
  position: relative;
  padding: clamp(72px, 12vw, 116px) 0 clamp(26px, 5vw, 44px);
  border-bottom: 1px solid var(--rule);
  background: radial-gradient(900px 420px at 22% -12%, rgba(142,52,40,.14), transparent 62%);
}
.page-head h1 { font-size: clamp(2.1rem, 5vw, 3.1rem); }
.page-head p { color: var(--muted); max-width: var(--measure); margin: .5em 0 0; font-size: var(--t-lead); }
.page-head .eyebrow { margin-bottom: .5em; }

/* Table of contents / jump chips */
.toc { display: flex; flex-wrap: wrap; gap: 10px; margin: 26px 0 0; }
.toc a {
  padding: 8px 15px; border-radius: 999px; border: 1px solid var(--rule);
  background: var(--surface); color: var(--muted); font-size: .9rem;
}
.toc a:hover { border-color: var(--ink); color: var(--ink); text-decoration: none; }

/* Prose column for the Guide */
.prose { max-width: var(--measure); }
.prose > p, .prose li { color: var(--muted); }
.prose h3 { margin-top: 1.8em; color: var(--ink); }
.prose strong { color: var(--ink); }
.prose a { color: var(--ink); }
.prose code, .kbd {
  background: var(--surface); border: 1px solid var(--rule); border-radius: 6px;
  padding: 2px 7px; font-size: .86em; color: var(--ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
/* A command to type, not a term mentioned in a sentence. The inline `code`
   pill above is wrong inside a block — its border and padding land on every
   line — so the block resets them and keeps only the surface. */
.prose pre {
  background: var(--surface); border: 1px solid var(--rule); border-radius: 10px;
  padding: 14px 16px; margin: 16px 0; overflow-x: auto;
  font-size: .86rem; line-height: 1.6; color: var(--ink);
}
.prose pre code {
  background: none; border: 0; padding: 0; font-size: inherit;
}
.callout {
  border-left: 3px solid var(--oxide); background: var(--surface);
  padding: 14px 18px; border-radius: 0 10px 10px 0; margin: 18px 0; color: var(--muted);
}
.callout strong { color: var(--ink); }

/* External-tool cards in the Guide */
.tool { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius); padding: 24px; margin: 0 0 16px; }
.tool h3 { margin-top: 0; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.tool p { color: var(--muted); margin: .4em 0 0; }
.tool ul { color: var(--muted); margin: .6em 0 0; padding-left: 1.1em; }

/* Changelog releases */
.release { border-left: 2px solid var(--ink); padding: 2px 0 2px 22px; margin: 0 0 34px; }
.release .ver { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
                font-size: .82rem; letter-spacing: .04em; color: var(--ink);
                font-weight: 650; margin: 0 0 2px; }
/* Releases are prepended, so the first article is always the newest -- the badge
   stays correct with no generator support and no back-editing of old entries. */
.release:first-of-type .ver::after { content: " · Latest"; color: var(--muted); font-weight: 400; }
.release h2 { font-size: 1.3rem; margin: 0 0 3px; }
.release .when { color: var(--muted); font-size: .8rem; text-transform: uppercase; letter-spacing: .1em; margin: 0 0 14px; }
.release ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 9px; }
.release li { color: var(--muted); padding-left: 20px; position: relative; }
.release li::before { content: "›"; position: absolute; left: 2px; color: var(--ink); font-weight: 700; }
.release li strong { color: var(--ink); font-weight: 650; }

/* FAQ — native <details> accordion */
.qa { border: 1px solid var(--rule); border-radius: 12px; background: var(--surface); margin: 0 0 12px; overflow: hidden; }
.qa > summary {
  cursor: pointer; padding: 18px 22px; font-weight: 650; color: var(--ink);
  list-style: none; display: flex; justify-content: space-between; align-items: center; gap: 16px;
}
.qa > summary::-webkit-details-marker { display: none; }
.qa > summary::after { content: "+"; color: var(--ink); font-size: 1.4rem; line-height: 1; flex: none; }
.qa[open] > summary::after { content: "\2013"; }
.qa .a { padding: 0 22px 20px; color: var(--muted); }
.qa .a a { color: var(--ink); }
.qa .a p { margin: 0 0 .7em; }
.qa .a p:last-child { margin-bottom: 0; }

/* Before / after image pair */
.ba { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
@media (max-width: 760px) { .ba { grid-template-columns: 1fr; } }

/* ===== Flair — calm, subtle motion (progressive enhancement) ===== */

/* Cards & tools gain a quiet teal-tinted edge and shadow on hover */
.card, .req, .tool { transition: border-color .3s ease, box-shadow .3s ease, transform .3s cubic-bezier(.22,1,.36,1); }
.card:hover, .req:hover, .tool:hover { border-color: rgba(255,255,255,.22); box-shadow: none; }

/* Nav links: a growing teal underline rather than the default text-underline */
.nav-links a { position: relative; }
.nav-links a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -5px; height: 1.5px; border-radius: 2px;
  background: var(--ink); transform: scaleX(0); transform-origin: left;
  transition: transform .3s cubic-bezier(.22, 1, .36, 1);
}
.nav-links a:hover { text-decoration: none; }
.nav-links a:hover::after { transform: scaleX(1); }

/* FAQ +/- marker eases */
.qa > summary::after { transition: color .2s ease, transform .25s ease; }

@media (prefers-reduced-motion: no-preference) {
  /* Gentle scroll-reveal — main.js adds .reveal to below-the-fold blocks,
     then .is-in when they scroll into view. Above-fold blocks are left alone. */
  .reveal { opacity: 0; transform: translateY(16px);
            transition: opacity .7s cubic-bezier(.22, 1, .36, 1), transform .7s cubic-bezier(.22, 1, .36, 1); }
  .reveal.is-in { opacity: 1; transform: none; }

  /* Cards lift a little on hover */
  .card:hover, .req:hover, .tool:hover { transform: translateY(-3px); }

  /* A quietly living hero sky — the faint star layer breathes and drifts, very slowly */
  @keyframes hero-sky {
    0%, 100% { opacity: .7; transform: translate3d(0, 0, 0); }
    50%      { opacity: 1;  transform: translate3d(-1.1%, .7%, 0); }
  }
  .hero::before { animation: hero-sky 30s ease-in-out infinite; }
}

/* ============================================================
   Tool pages: page header, before/after slider, tap grid
   ============================================================ */
/* Left-aligned. Centred before, which put a centred head over a left-aligned
   text column — two pages stacked — and is the centred-everything tell besides.
   The WIDTH is not set here: it comes from the markup, so the head sits on the
   same column as whatever the page puts underneath it. A prose page writes
   `wrap prose`, a wide page writes `wrap`. Setting it here instead centred a
   780px head above a 1280px grid, which is the same misalignment in reverse. */
.subhead { padding: clamp(64px, 10vw, 104px) 0 8px; }
.subhead h1 { font-size: clamp(2.1rem, 5vw, 3.1rem); }
.subhead .lead { max-width: var(--measure); margin: .5em 0 0; }

/* Tool-page rhythm. The screenshot sits close under the head and close over
   the body: as two ordinary .section blocks its own bottom padding plus the
   body's top padding stacked to ~150px of nothing between a caption and the
   text it captions. */
.section-shot { padding: clamp(24px, 4vw, 40px) 0 0; }
.section-body { padding: clamp(36px, 5vw, 56px) 0 clamp(56px, 9vw, 96px); }

/* Tool-page furniture. The download used to be a centred button in its own
   full-width section on every tool page — nine identical closing slabs. It is
   now a line at the foot of the text column, separated by a rule. */
.tool-cta { margin-top: 2.6em; padding-top: 1.7em; border-top: 1px solid var(--rule); }
.tool-cta .btn { display: inline-flex; }
.tool-shot-note { color: var(--muted); font-size: var(--t-small); max-width: 62ch; margin: 16px 0 0; }
/* .ba-solo is a 1040px box centred in its wrap, so a note left-aligned to the
   wrap starts 140px to the left of the picture it describes. Match the box. */
.ba-solo + .tool-shot-note { max-width: 1040px; margin: 16px auto 0; }
.tool-body .s-rail { margin: 0 0 2.4em; }

.crumbs { font-size: .85rem; color: var(--muted); margin-bottom: 8px; }
.crumbs a { color: var(--muted); }

/* Before/after slider */
.ba {
  position: relative; margin: 0; overflow: hidden; cursor: ew-resize;
  border-radius: var(--radius); border: 1px solid var(--rule);
  background: #05070f; line-height: 0; touch-action: none; user-select: none;
  --pos: 50%;
}
.ba-img { display: block; width: 100%; height: 100%; object-fit: cover;
  position: absolute; inset: 0; pointer-events: none; }
/* Both layers are absolute so they size against the aspect-ratio box; the
   container's height comes from aspect-ratio, not from an in-flow child. */
.ba-after { z-index: 0; }                       /* base layer (bottom) */
.ba-before { z-index: 1; clip-path: inset(0 calc(100% - var(--pos)) 0 0); }
.ba-divider, .ba-handle { z-index: 2; }
.ba-divider { position: absolute; top: 0; bottom: 0; left: var(--pos);
  width: 2px; background: rgba(255,255,255,.85); transform: translateX(-1px);
  pointer-events: none; box-shadow: 0 0 0 1px rgba(0,0,0,.35); }
.ba-handle { position: absolute; top: 50%; left: 50%; width: 38px; height: 38px;
  transform: translate(-50%,-50%); border-radius: 50%;
  background: rgba(10,16,32,.7); border: 2px solid rgba(255,255,255,.9);
  backdrop-filter: blur(4px); }
.ba-handle::before, .ba-handle::after { content: ""; position: absolute; top: 50%;
  width: 0; height: 0; border: 5px solid transparent; }
.ba-handle::before { left: 6px; border-right-color: #fff; transform: translateY(-50%); }
.ba-handle::after  { right: 6px; border-left-color: #fff; transform: translateY(-50%); }
.ba-range { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0;
  opacity: 0; pointer-events: none; -webkit-appearance: none; appearance: none; background: none; }
.ba-range:focus-visible { outline: none; }
.ba:focus-within .ba-handle { border-color: var(--ink); }
/* z-index 2, with the divider. Without it the tags sit at `auto` while
   .ba-before sits at 1, so the BEFORE image painted over its own label: every
   slider on the site showed only its right-hand tag, and the left one was there
   in the DOM, "visible", and covered. Found 2026-09-08 when the guide's
   narrowband slider showed "NARROWBAND" and nothing opposite it. */
.ba-tag { position: absolute; bottom: 10px; z-index: 2; padding: 3px 9px; font-size: .72rem;
  font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: #fff;
  line-height: 1.4;
  background: rgba(5,7,15,.6); border-radius: 999px; pointer-events: none; backdrop-filter: blur(4px); }
.ba-tag-before { left: 10px; } .ba-tag-after { right: 10px; }

/* Enhancement tap grid */
.wrap-wide { max-width: 1440px; margin: 0 auto; padding: 0 24px; }
.tap-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(560px, 1fr)); gap: 34px 28px; }
.tap { display: flex; flex-direction: column; }
.tap .ba { aspect-ratio: 1536 / 850; }
@media (max-width: 640px) { .tap-grid { grid-template-columns: 1fr; } }
.tap h3 { margin: 12px 0 2px; }
.tap p { margin: 0; color: var(--muted); font-size: .92rem; }
.group-label { grid-column: 1 / -1; margin: 8px 0 -6px; font-size: .8rem;
  text-transform: uppercase; letter-spacing: .08em; color: var(--ink); font-weight: 700; }

/* Sample data cards — image beside the facts, stacking on a narrow screen */
.samples { display: grid; gap: 34px; }
.sample { display: grid; grid-template-columns: minmax(0, 420px) 1fr; gap: 28px;
  align-items: start; padding: 22px; border: 1px solid var(--rule);
  border-radius: var(--radius); background: var(--surface); }
.sample-shot { margin: 0; }
.sample-shot img { width: 100%; height: auto; display: block; border-radius: var(--radius); }
.sample-body h3 { margin: 0 0 6px; }
.sample-body p { margin: 0 0 14px; color: var(--muted); }
.sample-body .btn { margin: 0 8px 8px 0; }
@media (max-width: 760px) { .sample { grid-template-columns: 1fr; } }

/* Tools index cards */
.tool-shot { margin: 0; }
.tool-shot img { display: block; width: 100%; height: auto;
  border-radius: var(--radius); border: 1px solid var(--rule);
  box-shadow: 0 24px 70px rgba(0,0,0,.45); }
.tool-shot figcaption { margin-top: 14px; text-align: center; color: var(--muted); font-size: .9rem; }
.tool-body { max-width: var(--measure); }
.tool-body h2 { font-size: 1.3rem; margin: 1.6em 0 .3em; }
.tool-body h2:first-child { margin-top: 0; }
.tool-body ol, .tool-body ul { padding-left: 1.2em; }
.tool-body li { margin: .3em 0; }
.deltas { width: 100%; border-collapse: collapse; margin: 1.4em 0; font-size: .95rem; }
.deltas th, .deltas td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--rule); }
.deltas th { color: var(--ink); font-weight: 650; }
.deltas td { color: var(--muted); }
.deltas td:last-child { text-align: right; font-variant-numeric: tabular-nums; color: var(--ink); }
.tool-body blockquote { margin: 1.6em 0; padding: 16px 20px; border-left: 3px solid var(--ink);
  background: var(--surface); border-radius: 0; }
.tool-body blockquote p { margin: 0; }
.tool-body h3 { margin-top: 1.6em; }
.tool-body figure.tool-shot { margin: 1.4em 0; }
.tool-body .ba { aspect-ratio: 1536 / 850; margin: 8px 0 4px; }

/* Standalone hero before/after slider (Auto Enhance page) */
.ba-solo { aspect-ratio: 1400 / 780; max-width: 1040px; margin: 0 auto; }

/* Masonry gallery of finished results (CSS columns — no JS dependency) */
.masonry { columns: 2; column-gap: 20px; }

/* --- Showcase banner ---
   Full-bleed, because the mosaic's whole point is width: boxing a 1.85:1 frame
   into the 1080px column would throw away the thing being demonstrated. */
.showcase { padding-bottom: clamp(28px, 4vw, 48px); }
.banner { margin: 0; }
.banner img {
  display: block; width: 100%; height: auto;
  border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
}
.banner figcaption {
  max-width: var(--wrap); margin: 14px auto 0; padding: 0 20px;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
}
.banner figcaption strong { font-weight: 650; }
.banner figcaption span { color: var(--muted); }
.banner-data { flex-basis: 100%; font-size: .82rem; font-variant-numeric: tabular-nums; }

/* --- Gallery ---
   Photographs, not screenshots, so the chrome steps back: no surface card, a
   hairline on the image itself, and the capture data as quiet type beneath.
   A bordered panel around a nebula competes with it. */
.gallery { columns: 3; column-gap: clamp(14px, 2vw, 26px); }
@media (max-width: 900px) { .gallery { columns: 2; } }
@media (max-width: 560px) { .gallery { columns: 1; } }

/* Multicol, not a grid with computed row spans. The span approach needs the
   column width at build time, and the column width depends on the viewport —
   so every size but one was wrong, and captions overlapped the next picture.
   Multicol balances at any width, at the cost of columns that end unevenly. */
.frame { break-inside: avoid; margin: 0 0 clamp(20px, 2.6vw, 32px); }
.frame-img {
  display: block; overflow: hidden; border-radius: var(--radius);
  border: 1px solid var(--rule); background: var(--surface);
}
.frame-img img { display: block; width: 100%; height: auto; cursor: zoom-in; }
.frame figcaption {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
  padding: 10px 2px 0;
}
.frame-target { font-weight: 650; letter-spacing: -.01em; }
.frame-common { color: var(--muted); }
.frame-data {
  flex-basis: 100%; color: var(--muted); font-size: .82rem;
  /* aligned digits: the numbers are the point of this line */
  font-variant-numeric: tabular-nums; letter-spacing: .01em;
}

@media (prefers-reduced-motion: no-preference) {
  .frame-img img { transition: transform .5s cubic-bezier(.22,1,.36,1); }
  .frame-img:hover img, .frame-img:focus-visible img { transform: scale(1.03); }
}
.frame-img:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
.masonry .shot { break-inside: avoid; margin: 0 0 20px; width: 100%; }
.masonry .shot img { cursor: zoom-in; }
@media (max-width: 640px) { .masonry { columns: 1; } }


/* ---- Type details ---------------------------------------------------------
   Running text near 65 characters; mono for anything that is data rather than
   prose; tabular figures wherever numbers sit in a column. */
p { max-width: 66ch; }

.eyebrow {
  font-family: var(--mono);
  font-size: var(--t-micro);
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--oxide-text);
  margin: 0 0 .6em;
}

.banner-data, .num, table td, .s-rail b { font-variant-numeric: tabular-nums; }

/* ---- Section shapes -------------------------------------------------------
   Five shapes, five jobs, replacing the one card grid that was repeated down
   every page. A page uses what fits its content; no page uses all five. */

.s-bleed { position: relative; }
.s-bleed img {
  display: block; width: 100%; height: auto;
  /* Capped: at 2200x1189 across a wide screen this was ~1080px tall and pushed
     everything below the fold. object-fit keeps the crop honest. */
  max-height: 74vh; object-fit: cover; object-position: center 78%;
}
.s-bleed .cap {
  /* Spans the WHOLE image, with the text sat on its floor. Anchored to the
     bottom edge instead, the lateral scrim below started at the caption's top
     and drew a hard horizontal seam across the photograph. Full-height, the
     darkening is one continuous left-to-right wash. */
  position: absolute; inset: 0;
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: clamp(28px, 4vw, 60px) clamp(20px, 4vw, 38px) clamp(20px, 4vw, 38px);
  /* The scrim has to be dark BEHIND THE TOP LINE, not just at the bottom edge.
     It used to run .94 -> transparent over 12%, so the eyebrow -- the only line
     naming who the app is for -- was printed onto bare star field and could not
     be read (measured on the live page 2026-09-08). --oxide-text is 5.06:1
     against --ground and was never the problem; the ground was.

     It scrims SIDEWAYS, not downward. A full-width vertical scrim tall enough
     to cover the headline also covers M 31 -- the first attempt at this fix
     left a page of bare stars with the galaxy blacked out. The text lives in
     the left 52ch, so that is the only part that needs a ground; the subject
     sits right of centre and keeps its own light. The stops are in px, not %,
     because what they have to cover is a ~540px text column, not a fraction of
     the window -- as percentages the gradient that read well at 1280 left the
     last words of the lead on a .13 scrim at 820px wide. */
  align-items: center; text-align: center;
  /* Contrast comes from a shadow on the GLYPHS, not from darkening the picture.
     A centred caption sits in the middle of the frame, where the subject is, so
     any scrim strong enough to carry the text also blots out the galaxy -- the
     text runs 38%-95% of the image height and M 31's core sits at 51%, so there
     is no scrim geometry that covers one and spares the other. A text-shadow
     has no such conflict: it darkens only the few pixels around each letter.
     The wash below is then free to stay gentle. */
  background: linear-gradient(to top,
    rgba(10,10,12,.62), rgba(10,10,12,.20) 45%, rgba(10,10,12,0) 78%);
}
.s-bleed .cap h1, .s-bleed .cap h2 {
  margin: 0 0 14px;
  font-size: clamp(2.1rem, 4.6vw, 4.1rem);
  max-width: 22ch; margin-inline: auto;
}
.s-bleed .cap .eyebrow, .s-bleed .cap h1,
.s-bleed .cap h2, .s-bleed .cap .lead {
  text-shadow: 0 1px 3px rgba(10,10,12,.95), 0 2px 18px rgba(10,10,12,.85),
               0 0 42px rgba(10,10,12,.7);
}
/* --oxide-text is 5.06:1 against --ground, which is the right number for a
   panel and the wrong one for a star field. Over the photograph it gets the
   same hue lifted until it reads there too. */
.s-bleed .cap .eyebrow { color: #E08878; }
/* The sentence that says what the thing IS. --muted is a panel colour and goes
   soft over a photograph, so the hero takes a brighter one. */
.s-bleed .cap .lead {
  color: #C9C4BB; max-width: 70ch; font-size: var(--t-lead);
  margin: 0 auto; text-align: center;
}
.s-bleed .cap .meta { max-width: 60ch; }
.s-bleed .cap .meta {
  font-family: var(--mono); font-size: var(--t-micro); letter-spacing: .18em;
  text-transform: uppercase; color: var(--muted); margin: 0;
}

/* The action band under the photograph. Left-aligned so the button hangs off
   the headline above it rather than floating in the middle of the page. */
.hero-act { padding: clamp(22px, 2.8vw, 32px) 0 clamp(30px, 4.5vw, 52px); }
.hero-act .cta { justify-content: center; }
.hero-act .meta {
  font-family: var(--mono); font-size: var(--t-micro); letter-spacing: .18em;
  text-transform: uppercase; color: var(--muted);
  text-align: center; margin: 20px auto 0; max-width: 60ch;
}

/* Under 1000px the caption is TALLER than the image it is positioned inside, so
   an absolute caption overflows upward and slides under the sticky nav -- the
   page opened with the words "did the hard part.", the eyebrow and the first
   line of the headline hidden behind the menu. Below this width the caption
   leaves the photograph and stacks under it. The threshold is where the image
   stops being tall enough to hold the caption. The image is min(0.54w, 0.72h)
   -- 0.54 because the source is 2200x1189 -- and the caption measures ~400px,
   so BOTH terms need a guard. Width alone is not enough: 1200x480 is wide but
   short, and there the caption escaped the image by 82px and put the eyebrow
   back under the nav. 680px of viewport height leaves 490px of image. */
@media (max-width: 1000px), (max-height: 680px) {
  .s-bleed img { max-height: 30vh; }
  .s-bleed .cap {
    position: static; background: var(--ground);
    padding: clamp(22px, 6vw, 32px) clamp(18px, 5vw, 24px) 0;
  }
  .hero-act .cta { justify-content: center; }
  .hero-act .cta .btn { flex: 1 1 100%; }
}

.s-split { display: grid; grid-template-columns: 1fr 1fr; border-top: 1px solid var(--rule); }
.s-split > * { padding: clamp(24px, 3.4vw, 40px); border-right: 1px solid var(--rule); }
.s-split > *:last-child { border-right: 0; }
.s-split img { display: block; width: 100%; height: auto; }
.s-split .flush { padding: 0; }

.s-rail {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
}
.s-rail div { padding: 13px 16px; border-right: 1px solid var(--rule); }
.s-rail div:last-child { border-right: 0; }
.s-rail b {
  display: block; font-family: var(--mono); font-size: 1.02rem;
  font-weight: 500; font-variant-numeric: tabular-nums;
}
.s-rail span {
  font-family: var(--mono); font-size: var(--t-micro); letter-spacing: .15em;
  text-transform: uppercase; color: var(--muted);
}

.s-index ol, .s-index ul {
  list-style: none; margin: 0; padding: 0; display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 1px; background: var(--rule);
}
.s-index li { background: var(--ground); padding: 16px 15px; }
/* When a cell is a link the whole cell is the target, not just the words.
   The negative margin cancels the li padding so the hit area fills the cell. */
.s-index li:has(> a) { padding: 0; }
.s-index li > a { display: block; padding: 16px 15px; color: inherit; }
.s-index li > a:hover { background: var(--surface); text-decoration: none; }
.s-index li > a:hover b { color: var(--oxide-text); }
/* Nine tools divide evenly into three columns; auto-fit would leave a ragged
   last row. Used where the count is known, not as a default. */
.s-index.cols-3 ul { grid-template-columns: repeat(3, 1fr); }
/* A last item with no neighbours fills its row rather than leaving two thirds
   of a rule-coloured band behind it. Fires only when the count leaves exactly
   one over, so it costs nothing at 9 or 12. */
.s-index.cols-3 li:last-child:nth-child(3n + 1) { grid-column: 1 / -1; }
/* The same idea for a remainder of TWO, which the rule above does not catch.
   The comment above says "nine tools divide evenly into three columns" — there
   are eleven now, and the eleventh left a rule-coloured hole in the corner of
   the grid that Andreas read as the page being cut off (2026-09-17). */
.s-index.cols-3 li:last-child:nth-child(3n + 2) { grid-column: span 2; }

/* A grid does not end with a margin the way prose does, so a full-width band
   after one starts flush against its last row: measured 0px on tools.html
   against 114px on stacking.html, where the space is the last paragraph's own
   bottom margin. Give the section the bottom padding its content cannot
   provide. Scoped with :has (the idiom this file already uses above) so the
   fifteen prose pages keep the spacing they have. */
.section-shot:has(.s-index) { padding-bottom: clamp(24px, 4vw, 40px); }
/* The gallery's masonry ends flush against whatever follows — measured, the gap
   was exactly 0px, which reads as the bottom row being CUT by the band rather
   than merely crowded by it. `.section-shot` has no bottom padding on purpose
   (elsewhere it holds one screenshot that should sit tight), so this is scoped
   the same way the rule above is. */
.section-shot:has(.gallery) { padding-bottom: clamp(36px, 5vw, 72px); }
/* Descriptions here are sentences, not data labels — mono over two lines reads
   as terminal output rather than prose. */
.s-index.cols-3 span { font-family: inherit; letter-spacing: 0; font-size: var(--t-small); }
@media (max-width: 760px) { .s-index.cols-3 ul { grid-template-columns: 1fr 1fr; } }
@media (max-width: 460px) { .s-index.cols-3 ul { grid-template-columns: 1fr; } }
.s-index b { display: block; font-size: .96rem; font-weight: 600; margin-bottom: 3px; }
.s-index span {
  font-family: var(--mono); font-size: var(--t-micro); color: var(--muted); letter-spacing: .07em;
}

.s-statement {
  background: var(--surface); border-top: 1px solid var(--rule);
  padding: clamp(28px, 4vw, 44px);
}
/* When the statement holds a .wrap, the wrap owns the horizontal inset. Both
   applying it put the statement 28-52px right of the prose it interrupts —
   invisible on desktop, where the inner column is centred, and obvious on a
   phone, where it is not. */
.s-statement:has(> .wrap) { padding-left: 0; padding-right: 0; }
.s-statement p {
  font-size: clamp(1.05rem, 2.1vw, 1.32rem); max-width: 52ch;
  margin: 0 0 10px; line-height: 1.45;
}
.s-statement cite {
  font-family: var(--mono); font-size: var(--t-micro); letter-spacing: .16em;
  text-transform: uppercase; color: var(--muted); font-style: normal;
}

@media (max-width: 720px) {
  .s-split { grid-template-columns: 1fr; }
  .s-split > * { border-right: 0; border-bottom: 1px solid var(--rule); }
}

/* ---------- Landing page: one centred axis ----------
   Scoped to .lp (index.html only). Before this, scrolling the landing page
   crossed five different left edges at 1920: the hero text at 38, the download
   buttons at 257, the before/after caption at 643 (centred), five sections at
   337, the gallery heading at 609 and Get Nocturne at 814. Andreas: "you scroll
   thru a webpage where things are centered and then all of a sudden you come
   across something that is very far to the left, but still not all the way to
   the left". The complaint was never left-versus-centre -- it was that the page
   had no single axis to read along. */
.lp > section > .wrap,
.lp > section > .wrap-wide { text-align: center; }
.lp > section > .wrap > h2, .lp > section > .wrap-wide > h2,
.lp > section > .wrap > .lead, .lp > section > .wrap-wide > .lead,
.lp > section > .wrap > p, .lp > section > .wrap-wide > p { margin-inline: auto; }

/* A figure sits on the axis as a block. */
.lp > section > .wrap > .ba,
.lp > section > .wrap-wide > .ba { margin-left: auto; margin-right: auto; }

/* Components keep their own internal alignment. A grid cell, a card, a photo
   caption and a form field are read as blocks -- centring the lines INSIDE them
   is a different thing from centring the block on the page, and only the second
   is what "centred" means here. The upload form especially: centred labels over
   left-aligned inputs give the eye two competing edges to track. */
.lp .s-index li, .lp .beat, .lp .req,
.lp .frame figcaption, .lp .contribute-form,
.lp .contribute-form label, .lp .consent { text-align: left; }
.lp .contribute-form { margin-left: auto; margin-right: auto; }

/* The statement is a pull-quote -- short enough that centring reads as emphasis
   rather than as a wall the eye has to re-find the start of. */
.lp .s-statement p, .lp .s-statement cite { margin-inline: auto; }

/* The footer follows its page's axis rather than having one of its own. It is a
   single shared component, so centring it everywhere would make it the only
   centred thing on the sixteen left-spine tool pages, and leaving it left makes
   it the only left thing under the landing page's centred statement -- 321px
   off, measured at 1440. A reader sees one page at a time, so the contradiction
   that matters is the one within a page. :has() is already the idiom here (see
   .s-statement:has(> .wrap)), and it means no build change: the footer reads the
   alignment off the <main> it belongs to. */
body:has(main.lp) .footer .wrap { text-align: center; }

/* THE FOOTER FOLLOWS ITS PAGE'S MEASURE. Measured 2026-09-20 at 2560px: a
   prose page centres its content in a 780px column at left 883, while the
   footer ran the full 1280px wrap at left 633 — two left edges 250px apart,
   which is what Andreas saw on the planner. It is not a planner bug; faq.html
   measures identically, and so does every page whose main content is .prose.
   The landing page keeps its own centred treatment above. */
body:not(:has(main.lp)):has(main .wrap.prose, main .planner) .footer .wrap {
  max-width: var(--measure);
}
body:has(main.lp) .footer p { margin-inline: auto; }

/* Downloads table (2026-09-18). Every release, newest first — so the columns
   have to stay readable at 38 rows and on a phone, where a three-column table
   is the first thing to break. The links column wraps to its own lines rather
   than squeezing, and the version column never wraps at all. */
.dl-table { width: 100%; border-collapse: collapse; margin: 0 0 2.4em; }
.dl-table th {
  text-align: left; font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: .12em; text-transform: uppercase; color: var(--muted);
  font-weight: 500; padding: 0 14px 10px 0; border-bottom: 1px solid var(--rule);
}
.dl-table td { padding: 12px 14px 12px 0; border-bottom: 1px solid var(--rule); vertical-align: top; }
.dl-table tr:first-child td { background: var(--surface); }
.dl-version { font-weight: 650; white-space: nowrap; }
.dl-date { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.dl-size { color: var(--muted); font-size: .82rem; font-variant-numeric: tabular-nums; }
.dl-latest {
  font-family: var(--mono); font-size: var(--t-micro); letter-spacing: .1em;
  text-transform: uppercase; color: var(--oxide-text); margin-left: 8px;
}
@media (max-width: 560px) {
  /* The date earns its place least on a narrow screen: the version orders the
     list and the links are the point. */
  .dl-table th:nth-child(2), .dl-table td:nth-child(2) { display: none; }
}

/* Where Nocturne looks for each tool (setup.html). Three columns of file paths,
   which a phone cannot show side by side and must not drop: the middle column
   here is macOS, not a date. Below 620px each row becomes its own block with
   the platform named, so nothing is hidden and the page never scrolls
   sideways. `code` wraps because a path has no spaces to break at and would
   otherwise set the width of the whole page. */
.path-table { width: 100%; border-collapse: collapse; margin: 0 0 2.4em; }
.path-table th {
  text-align: left; font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: .12em; text-transform: uppercase; color: var(--muted);
  font-weight: 500; padding: 0 14px 10px 0; border-bottom: 1px solid var(--rule);
}
.path-table td {
  padding: 12px 14px 12px 0; border-bottom: 1px solid var(--rule);
  vertical-align: top; color: var(--muted);
}
.path-table td:first-child { color: var(--ink); font-weight: 650; white-space: nowrap; }
.path-table code { overflow-wrap: anywhere; }
@media (max-width: 620px) {
  .path-table, .path-table tbody, .path-table tr, .path-table td { display: block; width: 100%; }
  .path-table thead { display: none; }
  .path-table tr { border-bottom: 1px solid var(--rule); padding: 14px 0 4px; }
  .path-table td { border: 0; padding: 0 0 10px; }
  .path-table td:first-child { font-size: 1.02rem; margin-bottom: 4px; }
  .path-table td[data-l="macOS"]::before, .path-table td[data-l="Linux"]::before {
    content: attr(data-l); display: block; font-family: var(--mono);
    font-size: var(--t-micro); letter-spacing: .12em; text-transform: uppercase;
    color: var(--muted); margin-bottom: 4px;
  }
}

/* --- Session planner (planner.html) --- */
.planner { max-width: var(--measure); }
.planner-where { max-width: 480px; margin: 26px 0; display: grid; gap: 10px; }
.planner-where label { font-size: var(--t-small); color: var(--muted); }
.planner-where input[type=text], .planner-where select {
  width: 100%; padding: 12px 14px; border-radius: 10px; border: 1px solid var(--rule);
  background: var(--surface); color: var(--ink); font: inherit;
}
.planner-where input:focus, .planner-where select:focus { outline: 2px solid var(--ink); outline-offset: 1px; }
.planner-where button { justify-self: start; }
/* The action sits BESIDE the box it acts on. It used to be at the foot of the
   form, below the telescope and the preferences, and Andreas's report was that
   it simply was not clear what to do: "pressing enter obviously works but its
   not clear". A search field with no visible action is a field people stare at. */
.place-row { display: flex; gap: 10px; align-items: stretch; }
.place-row input { flex: 1 1 auto; min-width: 0; }
.place-row button { flex: 0 0 auto; white-space: nowrap; }
.after-search { margin: 2px 0 6px; }
/* The privacy note moved BELOW the results. It used to sit between the box and
   the answer, so choosing between two candidate locations meant scrolling past
   a paragraph of disclosure to reach the list — on a phone especially, but he
   verified it is not only a phone problem. */
.privacy-note { margin-top: 28px; padding-top: 18px; border-top: 1px solid var(--rule); }
@media (max-width: 520px) {
  .place-row { flex-wrap: wrap; }
  .place-row button { width: 100%; }
}
.prefs {
  border: 1px solid var(--rule); border-radius: 10px; padding: 10px 14px 14px;
  display: flex; flex-wrap: wrap; gap: 14px;
}
.prefs legend { font-size: var(--t-small); color: var(--muted); padding: 0 4px; }
.prefs label { font-size: var(--t-small); color: var(--muted); display: grid; gap: 4px; }
.prefs select {
  padding: 10px 12px; border-radius: 8px; border: 1px solid var(--rule);
  background: var(--surface); color: var(--ink); font: inherit;
}
.prefs select:focus { outline: 2px solid var(--ink); outline-offset: 1px; }
#result { margin: 26px 0; }
#result h2 { text-transform: capitalize; }
#result .limiting { color: var(--muted); margin-top: -.4em; }
#result .warn { color: #ff6b6b; }
/* The verdict is the page's answer, so it is the one place colour carries
   meaning rather than decoration. Never colour ALONE: the word says it too,
   and the limiting factor beneath says why — about 8% of men cannot separate
   the go and no hues and would otherwise be reading an unlabelled signal. */
.verdict { margin: 0 0 .1em; }
.verdict.v-worth-going-out { color: var(--verdict-go); }
.verdict.v-marginal        { color: var(--verdict-warn); }
.verdict.v-skip            { color: var(--verdict-no); }
.verdict.v-can-t-say       { color: var(--muted); }

/* Targets are <details>, so the browser owns open/closed, focus and the
   keyboard. Collapsed, a row says exactly what the old card said; open, it
   adds what you want once you have chosen rather than while choosing. */
details.target { background: var(--surface); border: 1px solid var(--rule);
                 border-radius: 12px; margin: 10px 0; overflow: hidden; }
details.target > summary { list-style: none; cursor: pointer; padding: 14px 16px;
                           display: flex; gap: 14px; align-items: flex-start; }
details.target > summary::-webkit-details-marker { display: none; }
details.target > summary::after {
  content: "▸"; margin-left: auto; color: var(--muted); flex: 0 0 auto;
  transition: transform .15s ease; align-self: center;
}
details.target[open] > summary::after { transform: rotate(90deg); }
details.target > summary:hover { background: rgba(255,255,255,.02); }
details.target > summary:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }
.t-main { display: grid; gap: 3px; min-width: 0; }
.t-name { color: var(--ink); font-weight: 600; }
.t-when { color: var(--muted); font-size: var(--t-small); }
.t-fine { color: var(--muted); font-size: var(--t-micro); }
/* Right-aligned thumbnail, as asked. The slot collapses to nothing when there
   is no image, so the row does not hold space for something that is not there. */
.t-thumb { flex: 0 0 auto; width: 64px; height: 64px; object-fit: cover;
           border-radius: 8px; border: 1px solid var(--rule); margin-left: auto; }
.t-thumb + ::after { margin-left: 10px; }
.t-body { padding: 0 16px 16px; border-top: 1px solid var(--rule); margin-top: 2px; }
.t-facts { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px;
           margin: 14px 0 0; font-size: var(--t-small); }
.t-facts dt { color: var(--muted); }
.t-facts dd { margin: 0; color: var(--ink); }
@media (max-width: 460px) {
  .t-thumb { width: 48px; height: 48px; }
  .t-facts { grid-template-columns: 1fr; gap: 2px 0; }
  .t-facts dd { margin-bottom: 8px; }
}

.located { color: var(--muted); font-size: var(--t-small); margin: 0 0 18px; }
.located strong { color: var(--ink); }
.coords { color: var(--muted); font-variant-numeric: tabular-nums; }
.linkish {
  background: none; border: none; padding: 0; margin: 0; font: inherit;
  color: var(--ink); text-decoration: underline; cursor: pointer;
}
.linkish:hover { color: var(--muted); }
.hits { list-style: none; margin: 14px 0; padding: 0; display: grid; gap: 8px; }
.hits button {
  width: 100%; text-align: left; background: var(--surface); border: 1px solid var(--rule);
  border-radius: 8px; padding: 10px 14px; font: inherit; color: var(--ink); cursor: pointer;
}
.hits button:hover { border-color: var(--ink); }
.factors { list-style: none; margin: 18px 0; padding: 0; display: grid; gap: 8px; }
.factors li {
  padding: 10px 14px; border-radius: 8px; background: var(--surface);
  border: 1px solid var(--rule); color: var(--muted);
  display: flex; align-items: center; gap: 8px;
}
.factors li b { color: var(--ink); margin-right: 6px; }
.factors li.concern { border-color: var(--oxide); color: var(--oxide-text); }
.factors li.concern b { color: var(--oxide-text); }
/* Icons sit BESIDE the number they belong to, never instead of it -- carried
   over from sky.stehn.com.
   The slot is a FIXED box and planner.js emits it on every row, filled or not.
   This comment used to claim as much while nothing enforced it: the icons are
   34, 28 and 26 wide and three rows had none, so six stacked cards showed four
   different label offsets and read as a rendering fault. The box is 34 -- the
   cloud, the widest -- and every icon is centred in it, so each icon keeps its
   own deliberate size and the labels still share one left edge. */
.factors li .ficon {
  flex: 0 0 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
}
.factors li svg { flex: 0 0 auto; }
.factors li .wind { transition: none; }  /* rotation is data, not an animation cue */
#more {
  background: none; border: 1px solid var(--rule); border-radius: 999px; color: var(--ink);
  padding: 8px 16px; font: inherit; cursor: pointer; margin: 4px 0 14px; display: block;
}
#more:hover { border-color: var(--ink); }
.target {
  background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius);
  padding: 18px 20px; margin: 0 0 14px;
}
.target h3 { margin: 0 0 6px; }
.target p { color: var(--muted); margin: .3em 0 0; }
.nights {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: 22px 0 0; font-size: var(--t-small); color: var(--muted);
}
.nights button {
  background: none; border: 1px solid var(--rule); border-radius: 999px; color: var(--ink);
  padding: 8px 16px; font: inherit; cursor: pointer;
}
.nights button:hover { border-color: var(--ink); }
