/* ============================================================================
   wendl.ie — editorial catalogue
   ----------------------------------------------------------------------------
   ZERO EXTERNAL REQUESTS. Every face is subset and served from this origin.
   Not minimalism: every tool on this site is sold on "no data leaves your
   browser", and a page making that claim while pulling a font from Google is
   not one a careful reader would believe. netlify.toml sets default-src 'self'
   and tools/verify_site.py fails the built output if anything third-party
   appears.

   THE PALETTE IS NOT NEW. It is `AUTOMATION/scripts/generate_planner.py`'s
   print system, verbatim — the same ink the planners are drawn with. The site
   is therefore not a third identity: it joins the PRINT one, which is what it
   actually sells. The app's dark/sage world stays in the app.

   THE GOLD RULE IS INHERITED AND MEASURED. generate_planner.py: "Gold appears
   ONLY on dark pages, as line work and the mark. Never on a light page, and
   never as type colour." The maths agrees — gold as type on this ground is
   2.15:1 and unreadable; a gold BLOCK carrying near-black type is 7.77:1.
   So gold is a fill and a hairline here, never a text colour.
   ========================================================================== */

/* Display. One instanced cut of Archivo (OFL) — see tools/build_fonts.py.
   ⚠️ 400 FROM 7 Sep 2026, WAS 600. One cut replaced by another, ~10 KB either
   way, so no asset was added. Every display SIZE, tracking and leading is
   unchanged — only the weight moved. Reasoning in build_fonts.py; the short
   version is that a heavy face at 116px reads as volume and a light one at the
   same size reads as confidence.
   ⚠️ If you re-run build_fonts.py, ONLY 400 is instanced. A `font-weight:600`
   left anywhere on var(--display) will now be synthesised by the browser —
   faux-bold, which is the exact defect MASTER_STRATEGY §5 records the Studio
   chrome being moved off Geist Mono to avoid. Search before you add one. */
@font-face{
  font-family:"Archivo"; src:url("../fonts/archivo-400.woff2") format("woff2");
  font-weight:400; font-style:normal; font-display:swap;
}
/* Body. The face the planners embed, which is what keeps site and product
   visibly related. */
@font-face{
  font-family:"Geist"; src:url("../fonts/geist-400.woff2") format("woff2");
  font-weight:400; font-style:normal; font-display:swap;
}
@font-face{
  font-family:"Geist"; src:url("../fonts/geist-500.woff2") format("woff2");
  font-weight:500; font-style:normal; font-display:swap;
}
/* Metadata. Prices, page counts, permissions, IDs, build state. */
@font-face{
  font-family:"Geist Mono"; src:url("../fonts/geist-mono-400.woff2") format("woff2");
  font-weight:400; font-style:normal; font-display:swap;
}

:root{
  /* From generate_planner.py, verbatim. Do not drift these. */
  --paper:#F0EFEC;      /* light ground, never pure white */
  --ink:#1A1A1A;        /* near-black, all primary type */
  --ink-mid:#4A4A47;
  --ink-muted:#6B6B68;  /* captions, footers, secondary labels */
  --rule:#1A1A1A;       /* section separators, hairline */
  --rule-light:#D5D3CE; /* grid cells, writing lines */
  --paper-sunk:#E7E5E0; /* a shade down, for fields and hovers */

  --dark:#0A0A0A;       /* cover / opener ground — the blocks */
  --on-dark:#FFFFFF;
  --on-dark-mid:#B9B7B2;

  /* Line furniture and the mark. NEVER a text colour on paper. */
  --gold:#C9A84C;

  /* Links only. 4.98:1 on paper — the light-mode accent from the token file,
     not the app's #7A9E7E, which is too pale to read on this ground. */
  --sage:#547357;
  --sage-deep:#3F5A42;

  --display:"Archivo",ui-sans-serif,system-ui,-apple-system,"Segoe UI",Arial,sans-serif;
  --sans:"Geist",ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
  --mono:"Geist Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;

  --shell:1280px;
  --gutter:clamp(20px,4vw,40px);
  /* ⚠️ THE COLUMN SYSTEM, ADDED 7 Sep 2026, AND IT IS ONE SYSTEM ON PURPOSE.
     .section-head was `150px 1fr` and .row was `150px 1fr 170px`. They share a
     left edge and, because the row carries a third track, their content
     columns ended 206px apart at 1440 — so every section heading overhung its
     own rows. Measured across the page there were TWELVE distinct right edges
     (299, 307, 430, 531, 736, 748, 815, 884, 1019, 1102, 1243, 1269), and nine
     selectors are text-align:justify, which turns a soft ragged edge into a
     hard visible one. That is what "the alignment is off" was.
     Every grid on the page now draws from these three. Do not re-introduce a
     `1fr` content track: `1fr` means "whatever is left", which is a different
     width in a two-track grid than in a three-track one, and that difference
     IS the defect. */
  /* ⚠️ 132 / 620 FROM 7 Sep 2026, matching the Claude Design comp the owner
     approved. They were 150 / 720. The numbers are not arbitrary: 620px is the
     ONE measure every paragraph on the page shares, which is the thing that
     was wrong before — four different measures reading as four different
     columns. Change one of these and you change the whole page's alignment,
     which is the point of them being here rather than in each rule. */
  --col-label:132px;
  --col-body:620px;
  --col-act:170px;
  --col-gap:28px;
  --measure:60ch;
}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}}

body{
  margin:0;background:var(--paper);color:var(--ink);
  font:400 17px/1.6 var(--sans);
  -webkit-font-smoothing:antialiased;
  display:flex;flex-direction:column;min-height:100vh;
}

a{color:var(--sage);text-underline-offset:3px;text-decoration-thickness:1px}
a:hover{color:var(--sage-deep)}
:focus-visible{outline:2px solid var(--sage);outline-offset:3px;border-radius:2px}
::selection{background:var(--gold);color:var(--ink)}
img{max-width:100%;height:auto}

.skip{
  position:absolute;left:-9999px;top:0;background:var(--ink);color:var(--paper);
  padding:.7rem 1.1rem;font-weight:500;z-index:60;
}
.skip:focus{left:0}

.shell{max-width:var(--shell);margin:0 auto;padding:0 var(--gutter);width:100%}

/* ── masthead: Ventriloc's pill ───────────────────────────────────────────
   Five items — it was six until the arcade came out on 27 Aug 2026, and both
   this comment and base.njk's went on saying six until 28 Aug. The count lives
   in src/_data/site.json; read it there rather than trusting a comment. The
   pill is what stops a row of links reading as a corporate menu bar — it
   groups them into one object. */
.masthead{
  position:sticky;top:0;z-index:30;background:var(--paper);
  border-bottom:1px solid var(--rule-light);
}
.masthead-inner{
  max-width:var(--shell);margin:0 auto;padding:14px var(--gutter);
  display:flex;align-items:center;justify-content:space-between;
  gap:20px;flex-wrap:wrap;
}
.mark{display:flex;align-items:center;gap:9px;text-decoration:none;flex:none}
.mark img{display:block;height:44px;width:auto}
.mark-word{
  font:400 22px/1 var(--display);letter-spacing:-.02em;color:var(--ink);
}
.mark-tld{font:400 13px/1 var(--mono);color:var(--ink-muted)}

.nav-pill{
  display:flex;align-items:center;gap:2px;flex-wrap:wrap;
  background:var(--paper-sunk);border-radius:999px;padding:5px;
}
.nav-pill a{
  font:400 13px/1 var(--sans);color:var(--ink-mid);text-decoration:none;
  padding:9px 15px;border-radius:999px;
  /* .18s on the background, .15s on the colour. 150ms on both was the lower
     edge of comfortable for a pill that changes a SURFACE as well as its
     type: the fill arrived as a switch rather than as something settling.
     Not 300ms — navigation is a high-frequency target and slow is worse
     than abrupt here. 160-190ms is the band. */
  transition:background-color .18s ease,color .15s ease;
}
.nav-pill a:hover{color:var(--ink);background:#DCDAD4}
.nav-pill a[aria-current="page"]{background:var(--ink);color:var(--paper)}
@media(prefers-reduced-motion:reduce){.nav-pill a{transition:none}}

/* ── the mobile fold ──────────────────────────────────────────────────────
   The nav is a <details> so the burger needs no JavaScript. Above 56rem the
   summary is hidden and the pill is forced visible, so the desktop masthead
   is exactly what it was.

   ⚠️ `display:flex !important` ON THE PILL IS LOAD-BEARING, AND IT IS VERIFIED
   IN CHROMIUM ONLY. A closed <details> hides its own content, and at desktop
   the element is closed — nobody has clicked it. Whether author CSS can
   override that is genuinely browser-dependent: the hiding runs through an
   internal slot rather than a plain rule on the child, and engines differ.
   Measured at 1280px in Chromium: pill visible, five links clickable, summary
   hidden. NOT yet checked in Firefox or Safari.

   ⚠️ IF THE DESKTOP NAV IS EVER INVISIBLE IN ANOTHER BROWSER, THIS IS WHY, and
   the fix is structural rather than more CSS: move the <nav> OUT of the
   <details> to be its sibling, leave the summary alone inside it, and switch
   on `.nav-fold:not([open]) ~ .nav-pill{display:none}`. That never fights the
   UA stylesheet. It is not the default here only because keeping the nav
   inside the disclosure is the better semantic association.

   Worth checking by hand rather than trusting: the failure is silent and total
   — the navigation is present in the HTML and invisible on screen, which is
   precisely what verify_site.py reads straight past. */
.nav-fold{display:none}

@media(max-width:56rem){
  .masthead-inner{padding:12px var(--gutter)}
  .mark img{height:36px}
  .mark-word{font-size:19px}

  /* ⚠️ THE OPEN MENU IS A PANEL, NOT A ROW, AND BOTH OTHER ATTEMPTS FAILED.
     Measured at 390px:
       · fold sized to its content → the pill's max-content width is all five
         links in a row, 379px against 355px available, and the document
         scrolled sideways (docScrollWidth 399 v. a 390 viewport)
       · fold at width:100%       → no overflow, but it wraps onto its own line
         below the mark and the masthead goes back to 121px, which is the
         height this change exists to reclaim
     So the fold stays beside the mark at its own width, and the menu drops out
     of flow beneath the masthead. The masthead keeps one short row; the panel
     spans the gutters and cannot widen it. */
  .nav-fold{display:block;flex:none}
  .masthead-inner{position:relative}
  /* ⚠️ `display:flex` HERE IS WHAT RE-SHOWS THE PANEL, and it was missing for
     one build: the rule that used to carry it lived on the old child selector
     and went out with it, so the burger opened onto nothing. The pill's own
     `display:none` a few lines up is what it is overriding. */
  .nav-fold[open] ~ .nav-pill{
    display:flex;
    position:absolute;top:calc(100% + 12px);left:var(--gutter);right:var(--gutter);
    width:auto;margin-top:0;
    background:var(--paper);border:1px solid var(--rule-light);
    border-radius:14px;padding:8px;z-index:31;
    /* Top to bottom, one destination per row. The pill's horizontal wrap is
       right for a masthead and wrong for a panel: it left "Contact" alone on a
       second line looking like an afterthought rather than the fifth of five. */
    flex-direction:column;align-items:stretch;gap:2px;
  }
  .nav-fold[open] ~ .nav-pill a{border-radius:10px}

  /* The burger. A summary, so it is a real button: Enter and Space work, the
     expanded state is announced, and it survives scripts being blocked. */
  .nav-fold summary{
    display:flex;align-items:center;gap:8px;cursor:pointer;list-style:none;
    padding:11px 15px;border-radius:999px;background:var(--paper-sunk);
    font:400 13px/1 var(--sans);color:var(--ink-mid);
  }
  .nav-fold summary::-webkit-details-marker{display:none}
  .nav-fold summary svg{
    fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;
  }
  .nav-fold[open] summary{color:var(--ink);background:#DCDAD4}

  /* Closed is the default on a phone, so the pill only exists once opened. */
  .nav-pill{
    display:none;
    width:100%;margin-top:10px;justify-content:flex-start;flex-wrap:wrap;
  }
  

  /* 44px, not the 26-29px this measured before. That is the size a thumb
     actually wants; WCAG 2.2's 24px floor is a floor, not a target. */
  .nav-pill a{padding:14px 16px;font-size:13.5px;min-height:44px;
              display:flex;align-items:center}
}
@media(max-width:24rem){
  .nav-pill a{padding:13px 13px;font-size:13px}
}

/* ── sections ───────────────────────────────────────────────────────────
   THESE USE padding-block, NOT the padding shorthand, and it matters. Section
   classes are applied to the SAME element as .shell (`class="band shell"`), so
   a shorthand `padding: X 0` silently zeroes .shell's horizontal gutter - the
   classic cascade collision. It shipped once: at 320px every line ran to the
   screen edge with no margin at all. Keep the axes separate. */
/* ⚠️ NO BORDER-TOP, from 7 Sep 2026. Every band carried `border-top:1px solid
   var(--rule-light)`, so a 3,000px section and a 400px section announced
   themselves identically and the page read as a stack of equal competitors
   rather than as a studio with a secondary category.
   Both reference sites the owner chose separate by space and scale instead:
   dottxt.ai ships ZERO section borders, ventriloc.ca uses bands. The rules
   are gone and the SPACING now carries the rank — which is the only way a
   reader can tell what is major without being told.
   The hairlines that remain are the ones that separate PEERS: .rows between
   products, .receipt-row between facts. Those are inside a section, not
   between two, and they stay. */
.band{padding-block:clamp(48px,7vw,104px)}
/* The rank scale. Three steps, not a continuum — a continuum is how a spacing
   system turns back into "whatever looked right that day". */
.band-major{padding-block:clamp(72px,10vw,148px)}
.band-tight{padding-block:clamp(32px,4.5vw,56px)}

/* Full-bleed dark block. The planners' own alternation: dark covers and
   section openers, light working pages. This is where gold is allowed. */
.block{
  background:var(--dark);color:var(--on-dark);
  padding-block:clamp(52px,8vw,120px);border-top:0;
}
.block .eyebrow{color:var(--gold)}
.block a{color:var(--on-dark)}
.block a:hover{color:var(--gold)}
.block .rule{border-color:rgba(201,168,76,.42)}
.block-lead{
  margin:0;max-width:22ch;
  font:400 clamp(30px,4.4vw,60px)/1.06 var(--display);letter-spacing:-.028em;
  text-wrap:balance;
}
.block-body{
  margin:clamp(22px,3vw,32px) 0 0;max-width:52ch;
  color:var(--on-dark-mid);font-size:17px;line-height:1.7;text-wrap:pretty;
}
.block-body strong{color:var(--on-dark);font-weight:500}

.rule{border:0;border-top:1px solid var(--rule-light);margin:0}

/* ── the shared label device ──────────────────────────────────────────── */
/* THE FLAIR IS ONE CHARACTER, AND IT IS THE ONE THE SITE ALREADY OWNS.
   Added 31 Aug 2026. Every eyebrow takes a leading sage bullet — the same
   glyph that is already the brand in `page•pin`, already the accent in the
   `.dot` filter, and already the one character build_fonts.py subsets in
   specially (U+2022) because losing it would break every product name.

   So it is not a new signifier. It is the existing one used one layer wider,
   which is why it reads as flair rather than as decoration bolted on.

   ⚠️ NOT the `01 / 02 / 03` numbering. DESIGN-PROMPT.md §7 considered that on
   28 Aug and OVERRULED it: "those three sections are not a sequence, nobody
   does them in order, and numbering them would be the exact borrowed-signifier
   this list exists to refuse." Recorded here because a marker in the eyebrow
   slot is exactly where someone would re-propose it.

   ::before with an empty alt in the content string keeps it out of the
   accessibility tree, so a screen reader reads "EXTENSIONS", not
   "bullet EXTENSIONS". */
.eyebrow{
  margin:0 0 clamp(20px,2.6vw,30px);
  font:400 11px/1.5 var(--mono);letter-spacing:.18em;text-transform:uppercase;
  color:var(--ink-muted);
  /* FLEX, NOT AN INLINE ::before WITH A MARGIN. As an inline box the bullet
     was part of the text flow, so a long label wrapped AROUND it — "Who each
     one is for" on /work/ broke after the bullet and set the second line hard
     against the rail. As a flex item it is a fixed column the text wraps
     inside, so every eyebrow aligns on the same left edge however long it is.
     `align-items:baseline` keeps the bullet on the type baseline rather than
     centring it against a two-line block. */
  display:flex;align-items:baseline;gap:.75em;
}
.eyebrow::before{
  content:"\2022" / "";
  color:var(--sage);
  flex:none;
}
/* The eyebrow is a link on post pages ("Journal · 31 August 2026"), where flex
   would otherwise put the date on its own line. */
.eyebrow > *{min-width:0}
/* On dark the sage is too dim to register; gold is what the block already
   uses for its eyebrow, so the marker follows it rather than introducing a
   second accent on that ground. */
.block .eyebrow::before{color:var(--gold)}
.eyebrow-num{color:var(--ink);margin-right:.9em}
/* Modifiers, not inline styles: `style-src 'self'` carries no 'unsafe-inline',
   so a style="" attribute is dropped by the browser and the page silently
   renders without it. Two of them shipped that way until 28 Aug 2026. */
.eyebrow--flush{margin:0}

/* ── hero: asymmetric, text left, product right ───────────────────────── */

/* A faint ruling behind the hero, and only the hero.

   It is painted as the section's own BACKGROUND rather than as an overlay
   pseudo-element. An overlay with mix-blend-mode:multiply would sit on top of
   the type and darken it — imperceptibly at this opacity, but it would be
   compositing text against a texture for no reason. Behind is both cheaper
   and correct.

   4.5% ink on a 4px pitch. The reference is a CRT scanline; on paper at this
   weight it reads as laid stock, which is the right end of that idea for a
   site whose products are printed. Any heavier and it is grime — the reason
   this was declined once before applying it to the whole page. */
.hero{
  padding-block:clamp(44px,6.5vw,92px) clamp(48px,7vw,96px);
  background-image:repeating-linear-gradient(
    to bottom,
    rgba(26,26,26,.045) 0 1px,
    transparent 1px 4px
  );
}
.hero-grid{
  display:grid;gap:clamp(32px,5vw,64px);align-items:end;
  grid-template-columns:1fr;
}
/* min-width:0, and it is load-bearing at 320px.

   A `1fr` track is really `minmax(auto, 1fr)`, and that `auto` minimum is
   min-content — which the hero image sets, because it carries width="340".
   So the column resolved to 340px inside a 265px container and took the h1,
   the lead and the status line off the side of the screen with it. `img{
   max-width:100%}` cannot rescue that: 100% of a blown-out column is still
   blown out.

   Measured on 28 Aug 2026 at a 320px viewport: 55px of horizontal overflow,
   grid container 265px against a 340px track. This is the same failure the
   .band comment records shipping once before — a page that scrolls sideways
   on a phone — so it is fixed here rather than noted. */
.hero-grid > *{min-width:0}
@media(min-width:62rem){.hero-grid{grid-template-columns:1.35fr .8fr}}

.hero h1{
  margin:0;font-family:var(--display);font-weight:400;
  /* ⚠️ THE MAX IS 104px, NOT 116px — CORRECTED 7 Sep 2026, AND THE REASON IS
     A UNIT MISMATCH THE OLD VALUE HID. The size grows on 8vw, but the column
     it sits in does NOT: --shell caps at 1240px, so above about 1300px the
     text column is frozen at 658px while the type keeps growing. At 1440 that
     put 8 words on SIX lines of 115px type — a 636px wall — while the column
     beside it sat half empty.
     104px is the size at which the headline still sets in five lines at every
     width from 1280 up, which is how it read at 1280 before. A clamp whose
     max exceeds what its container can hold is not a clamp, it is a delay. */
  font-size:clamp(44px,8vw,104px);line-height:.92;letter-spacing:-.04em;
  max-width:13ch;text-wrap:balance;
}
.hero-lead{
  margin:clamp(24px,3.2vw,34px) 0 0;max-width:46ch;
  font-size:clamp(17px,1.5vw,19px);line-height:1.62;color:var(--ink-mid);
  text-wrap:pretty;
}
.hero-lead strong{color:var(--ink);font-weight:500}
.hero-meta{
  margin:clamp(22px,3vw,30px) 0 0;padding-top:16px;
  border-top:1px solid var(--rule-light);
  font:400 12px/1.7 var(--mono);letter-spacing:.03em;color:var(--ink-muted);
  font-variant-numeric:tabular-nums;max-width:44ch;
}

/* ⚠️ .hero-meta AND .hero-status BOTH DRAW A HAIRLINE ABOVE THEMSELVES, and
   from 9 Sep 2026 they sit one on top of the other — the counts, then the
   policy line. Two rules 16px apart read as a double rule someone drew by
   accident, so the second drops its own and tucks up under the first. */
.hero-meta + .hero-status{margin-top:0;padding-top:4px;border-top:0}

/* The status line. Inherits the meta treatment — mono, hairline above,
   tabular figures so the count cannot shift the line as it types — and adds a
   caret. Sage, which is what this palette already spends on the live dot and
   on "nothing", so a caret is consistent rather than a new colour. */
.hero-status{
  margin:clamp(22px,3vw,30px) 0 0;padding-top:16px;
  border-top:1px solid var(--rule-light);
  font:400 12px/1.7 var(--mono);letter-spacing:.03em;color:var(--ink-muted);
  font-variant-numeric:tabular-nums;
  /* 52ch, not the 44ch the meta line uses, and not 48 either — both were
     measured wrapping the CARET onto a line of its own while the text sat
     happily on one. The text is 44 characters, and with letter-spacing plus
     the caret and its margin it needs ~348px; 48ch caps at 346. Measured, not
     guessed: the element was 2.05 line-heights tall while the text occupied
     exactly one line box.
     It still wraps on a narrow screen, which is correct — there the caret
     follows the last line rather than starting a new one. */
  max-width:52ch;
}
.hero-status::after{
  content:"";display:inline-block;vertical-align:-2px;
  width:6px;height:13px;margin-left:9px;background:var(--sage);
}
/* An always-blinking caret is motion. Someone who asked for none gets a solid
   block, which still reads as a terminal and never moves. */
@media(prefers-reduced-motion:no-preference){
  .hero-status::after{animation:caret 1.1s steps(1) infinite}
}
@keyframes caret{50%{opacity:0}}

/* The caret used to be only this one, fixed after the last span. That span
   is already full-width the moment the line scrolls into view — [data-ch]
   spans reserve their layout space up front and only their opacity animates
   — so the fixed caret sat lit at the very end for the whole reveal while
   the letters popped in underneath it, rather than looking like it wrote
   them.
   Now every character gets its own caret, shown only for the one STEP it is
   "current", positioned absolutely so it adds no width of its own (position:
   relative is safe on [data-ch] because one character can never itself be
   split across a wrapped line, so each caret still lands on the correct
   line). The end-of-line caret above is suppressed for that same window —
   .is-typing sets it, .is-typed (added by type-in.js once the last
   character has revealed) hands it back — so exactly one caret is ever lit. */
.hero-status [data-ch]{position:relative}
.hero-status [data-ch]::after{
  content:"";position:absolute;top:4px;right:-7px;
  width:6px;height:13px;background:var(--sage);
  opacity:0;
}
@media(prefers-reduced-motion:no-preference){
  .hero-status.is-typing [data-ch]::after{
    animation:ch-caret var(--type-step,26ms) linear forwards;
    animation-delay:calc(var(--i,0) * var(--type-step,26ms));
  }
  .hero-status.is-typing:not(.is-typed)::after{
    opacity:0;animation:none;
  }
}
@keyframes ch-caret{
  0%,99.99%{opacity:1}
  100%{opacity:0}
}
.hero-figure{margin:0}
.hero-figure img{
  display:block;width:100%;aspect-ratio:.707;object-fit:cover;
  box-shadow:0 18px 44px rgba(26,26,26,.22);
}
.hero-figure figcaption{
  margin-top:12px;font:400 11px/1.6 var(--mono);letter-spacing:.05em;
  color:var(--ink-muted);
}

/* ── page•pin's sidebar, drawn ────────────────────────────────────────────
   The dark half of the house, sitting on the light half — the same trick the
   planner cover was doing in this slot, and the same one the .block bands do
   further down. The site is paper because it sells print; a TOOL is dark,
   because that is what the tool actually looks like.

   Every value below is the shipped v2.5 token set, copied rather than
   invented: bg #111111, surface #1a1a1a, borders #2e2e2e/#3d3d3d, ink
   #f0ede8/#a09c97/#6a6663, accent #7a9e7e with #4a6b4d dim, radius 12 outer
   and 8 inner, 6px gap, 13px pad. They are scoped to .pp so the app palette
   cannot leak into the paper one.

   ONE HONEST DEVIATION: the extension sets DM Mono, which is not on this
   domain and cannot be fetched — the whole site is one origin. This uses Geist
   Mono, which is. It is a drawing of the panel, not a copy of it, and the
   figcaption says as much by describing the tool rather than claiming a
   screenshot.

   aspect-ratio matches the cover it replaced, so the hero geometry did not
   move. min-width:0 on the grid children is what keeps this from blowing the
   column out at 320px — see .hero-grid. */
.pp{
  --pp-bg:#111111;--pp-surface:#1a1a1a;
  --pp-border:#2e2e2e;--pp-border-hi:#3d3d3d;
  --pp-ink-1:#f0ede8;--pp-ink-2:#a09c97;
  /* ⚠️ WAS #6a6663, RAISED 7 Sep 2026. It measured 3.06:1 on --pp-surface and
     3.32:1 on --pp-bg — below the 4.5:1 floor DESIGN-PROMPT.md §2e calls a
     floor rather than a target — and it is the colour of .pp-tab, .pp-search,
     .demo-chrome and .demo-bar-label, all real text in the live DOM.
     #8a8681 measures 4.67:1 on --pp-surface and 5.26:1 on --pp-bg. It is still
     the dimmest of the three inks, so the panel's depth ordering is unchanged;
     it is dim enough to recede and legible enough to read. */
  --pp-ink-3:#8a8681;
  --pp-accent:#7a9e7e;--pp-accent-dim:#4a6b4d;

  display:flex;flex-direction:column;
  /* .8, not the cover's .707. A sidebar is tall and narrow in a browser, but
     at A-series proportions in this slot the note area became a void — the
     panel is only as tall as it has content for. */
  /* ⚠️ THE RATIO WAS REMOVED ON 7 Sep 2026 AND PUT BACK THE SAME DAY. DO NOT
     REMOVE IT AGAIN. Dropping it took the panel from 487px to 248px, which did
     close the 268px gap and also destroyed the thing the hero is for: a tall
     narrow sidebar that looks like the product it depicts. The owner asked for
     it back in those words.
     The gap is real and is NOT solved here. It is solved by the panel having
     something to sit in — the spacing scale below — and, if it still reads as
     a void, by giving the demo a third note. The fix for an underfilled box is
     content, not a shorter box. */
  width:100%;aspect-ratio:.8;
  background:var(--pp-bg);
  border:1px solid var(--pp-border);
  border-radius:12px;
  padding:13px;gap:6px;
  font-family:var(--mono);
  box-shadow:0 18px 44px rgba(26,26,26,.22);
  overflow:hidden;
}
.pp-head{
  display:flex;align-items:center;justify-content:space-between;gap:8px;
  padding-bottom:9px;border-bottom:1px solid var(--pp-border);
}
.pp-mark{
  font-size:11px;letter-spacing:.16em;color:var(--pp-ink-1);
  text-transform:lowercase;
}
.pp-dot{color:var(--pp-accent)}
.pp-saved{
  font-size:8.5px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--pp-accent);
  background:color-mix(in srgb,var(--pp-accent) 10%,transparent);
  border:1px solid var(--pp-accent-dim);border-radius:8px;
  padding:3px 7px;white-space:nowrap;
}
.pp-tabs{display:flex;gap:12px;border-bottom:1px solid var(--pp-border)}
.pp-tab{
  font-size:9px;letter-spacing:.12em;text-transform:uppercase;
  color:var(--pp-ink-3);padding-bottom:7px;
  border-bottom:2px solid transparent;margin-bottom:-1px;
}
.pp-tab.is-on{color:var(--pp-ink-1);border-bottom-color:var(--pp-accent)}
.pp-body{flex:1;padding-top:4px;min-height:0}
.pp-note{
  margin:0 0 9px;font-size:11px;line-height:1.55;letter-spacing:.02em;
  color:var(--pp-ink-1);
}
.pp-note--dim{color:var(--pp-ink-2)}
/* Inline, at the end of the note — a text cursor sitting where you stopped
   typing, which is what the real panel shows. As its own block it just sat on
   an empty line underneath. */
.pp-caret{
  display:inline-block;width:2px;height:11px;background:var(--pp-accent);
  vertical-align:-1px;margin-left:2px;
}
@media(prefers-reduced-motion:no-preference){
  .pp-caret{animation:caret 1.1s steps(1) infinite}
}
.pp-tags{display:flex;flex-wrap:wrap;gap:5px}
.pp-tag{
  font-size:8.5px;letter-spacing:.08em;color:var(--pp-accent);
  background:color-mix(in srgb,var(--pp-accent) 10%,transparent);
  border:1px solid var(--pp-accent-dim);border-radius:8px;padding:3px 7px;
}
.pp-search{
  font-size:9px;letter-spacing:.04em;color:var(--pp-ink-3);
  background:var(--pp-surface);
  border:1px solid var(--pp-border-hi);border-radius:8px;
  padding:8px 9px;
}
/* Below the hero's two-column breakpoint the panel is the full column width,
   which is wider than it ever is in a browser. Cap it so it keeps the
   proportions of a real sidebar instead of stretching into a poster. */
@media(max-width:62rem){
  .pp{max-width:340px}
}

/* ── inner page heads ─────────────────────────────────────────────────── */
.page-head{padding-block:clamp(40px,6vw,84px) clamp(28px,4vw,48px)}
.page-head h1{
  margin:0;font-family:var(--display);font-weight:400;
  font-size:clamp(38px,6.2vw,84px);line-height:.96;letter-spacing:-.035em;
  max-width:15ch;text-wrap:balance;
}

/* ── section headings ─────────────────────────────────────────────────── */
.section-head{
  display:grid;gap:clamp(14px,2.5vw,40px);grid-template-columns:1fr;
  margin:0 0 clamp(28px,4vw,48px);
}
@media(min-width:52rem){
  .section-head{grid-template-columns:var(--col-label) var(--col-body)}
}

/* ── the measure, and the one place it is set ────────────────────────────
   Every running paragraph shares --col-body. Before this the page had four
   measures (748 / 884 / 1019 / 1063px right edges) and nine separate
   `text-align:justify` declarations, so each mismatch showed up as a HARD
   edge rather than a soft ragged one. One rule now.

   ⚠️ text-align-last:left IS LOAD-BEARING, and it is what the previous
   justification was missing. Without it the LAST line of a paragraph is
   justified too, so a three-word closing line gets stretched across the full
   measure — the single ugliest thing justification does. From the comp. */
.measure{
  max-width:var(--col-body);
  text-align:justify;
  text-align-last:left;
  hyphens:auto;-webkit-hyphens:auto;
  text-wrap:pretty;
}
/* ⚠️ .section-lead AND .block-lead ARE ON <h2> ELEMENTS, from 3 Sep 2026.
   They were <p> until then, which meant / and /work/ went <h1> straight to
   <h3> with no <h2> anywhere — the four section titles on the homepage were
   invisible to a screen reader's heading list and to a crawler's outline,
   while the tool names below them were h3s hanging off nothing.

   Both rules set `margin:0` and a full `font:` shorthand, so the UA's h2
   defaults are fully overridden and nothing moved on the page. Do NOT strip
   either back to a partial declaration: without the shorthand the browser's
   own h2 size returns and the section headings shrink. */
.section-lead{
  margin:0;max-width:44ch;
  font:400 clamp(24px,3vw,38px)/1.14 var(--display);letter-spacing:-.024em;
  text-wrap:balance;
}
/* The section note reads as an aside on a statement, so it is set as one:
   mono, a hairline rail, and a `//` in the margin. That is the second and last
   piece of code flair — the site's argument is restraint, and a page that
   annotates itself everywhere is a page that has started performing.

   The `//` is a pseudo-element, not markup, so it never reaches the
   accessibility tree, never lands in a copy-paste, and never appears in the
   `description` a search result shows. */
.section-note{
  margin:16px 0 0;max-width:52ch;
  padding-left:clamp(14px,1.6vw,20px);
  border-left:1px solid var(--rule-light);
  /* `position:relative` went with the "//" marker below — it existed only to
     be the containing block for that absolutely-positioned pseudo-element. */
  font:400 13px/1.7 var(--mono);
  color:var(--ink-muted);
  text-wrap:pretty;
}
/* ⚠️ A "//" MARKER LIVED HERE AND WAS REMOVED ON 3 SEP 2026, at the owner's
   request. It set `content:"//" / ""` in the left gutter — a code-comment
   glyph in --rule-light, with empty alt text so a screen reader skipped it.
   It was a deliberate detail, not an artifact.
   It read as debris rather than as a device: at --rule-light on paper it is
   faint enough to look like a rendering fault, and the reader has no reason to
   connect a source-code convention to a sentence about email. The left hairline
   already does the whole job of marking an aside.
   Restoring it is this comment plus four lines. Do not re-add it by accident. */
.section-note a{color:var(--sage)}

/* ── the tier head, added 7 Sep 2026 ──────────────────────────────────────
   The Software band carries two kinds of thing — browser tools and the paid
   tool — and they were two separate <section>s until this date, which put an
   "Extensions" heading first and let a visitor answer "what is this place"
   before reaching the second one. DECISIONS.md 2026-09-07.

   ⚠️ IT IS AN <h3>, AND THE PRODUCT NAMES UNDER IT MOVED TO <h4>. That is the
   entire reason this class exists rather than being a styled <p>: the band's
   own title is the h2, so a heading between it and the product names has to be
   an h3 or the outline skips a level. .row-name is styled by class, so nothing
   moved on the page when the element changed.

   Sized DOWN from .section-lead deliberately. A tier is a label on a group,
   not a second statement — set it near body size in the display face and it
   reads as structure. Bigger, and the band appears to have two headlines.

   No new token and no new colour: --ink-muted and --rule-light are what every
   other label on this page already uses. */
.tier{
  margin:clamp(30px,4vw,46px) 0 0;
  padding-bottom:10px;
  border-bottom:1px solid var(--rule-light);
  /* ⚠️ 400, NOT 500 OR 600. Only ONE Archivo cut is instanced, and it is
     400 — any other weight here is synthesised by the browser, which is
     faux-bold. The tier separates itself from the headings around it by
     COLOUR (--ink), by size (15px against 24-38px) and by the rule under
     it. It does not need weight, and weight is the one axis it cannot
     have. */
  font:400 15px/1.3 var(--display);letter-spacing:.005em;
  /* ⚠️ --ink, NOT --ink-muted, from 7 Sep 2026 — and the SIZE deliberately did
     not change with it. The note above is still right that a bigger tier gives
     the band two headlines. The defect was never size: it was that the tier
     was the same colour as the band's own .eyebrow and quieter than the 21-26px
     .row-name items it groups, so the label read as a caption on the first
     product rather than as a heading over four. Colour is what separates a
     heading from a caption here; size is what separates it from the h2. */
  color:var(--ink);
  text-transform:none;
}
/* The first tier sits directly under .section-head, which already carries the
   band's bottom margin. Doubling it there opened a gap the eye reads as a
   missing element. */
.section-head + .tier{margin-top:0}
/* A note under a tier is an aside on THAT tier, so it loses the band-level
   note's top margin and tucks under the rule instead. */
.tier + .section-note{margin-top:12px}

/* ── the tools: rows and rules, no cards ──────────────────────────────────
   THE SIGNATURE IS THE RECEIPT. Every extension listing on earth asserts that
   it respects your privacy; this prints the manifest instead. `asks` is copied
   from the shipped manifest.json and `sends` is verified against the source —
   no fetch, no XMLHttpRequest, no sendBeacon, no WebSocket in any of the three.

   Two of the three ask for every site. That is shown, not softened: "it has to
   reach the page you are pinning a note to" is a stronger argument than a
   privacy badge, and a curious reader checks the store listing anyway. */
.rows{border-top:1px solid var(--rule)}
.row{
  display:grid;gap:clamp(14px,2.5vw,40px);grid-template-columns:1fr;
  padding:clamp(26px,3.6vw,44px) 0;border-bottom:1px solid var(--rule-light);
}
@media(min-width:52rem){
  .row{grid-template-columns:var(--col-label) var(--col-body) var(--col-act);align-items:start}
  /* ⚠️ THE BUTTON ALIGNS TO THE BOTTOM, AND THIS IS NOT A TASTE CALL.
     With align-items:start the third cell pinned to the top of rows that run
     537-724px tall, leaving 451-646px of empty column under each button —
     2,064px of dead lane down a 6,439px page. The reader finished the receipt,
     which is the moment of maximum conviction, with no control anywhere near
     their eye. Measured 7 Sep 2026.
     Only the third cell moves: the name and the prose still start at the top,
     so the row still reads left-to-right as label, argument, action. */
  .row > :last-child{align-self:end}
}

.row-name{
  margin:0;font:400 clamp(21px,2.2vw,26px)/1.1 var(--display);
  letter-spacing:-.024em;
}
.row-does{
  margin:7px 0 0;font:400 11px/1.4 var(--mono);letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-muted);
}
.row-blurb{margin:0;max-width:48ch;color:var(--ink-mid);line-height:1.65;text-wrap:pretty}

/* The receipt, framed like an instrument readout.

   Corner brackets drawn as eight background gradients — four corners, two arms
   each. No extra element, no image, no script, so it costs nothing against the
   "loads nothing from anywhere else" claim and nothing against the CSS-only
   motion rule.

   The reference is the MGS codec panel, and the reason it earns a place HERE
   and nowhere else is that this block is already a readout: it prints the
   shipped manifest instead of asserting a privacy promise. Framing an
   instrument like an instrument is additive. The same texture on body copy
   would be decoration — which is why four of the five retro techniques
   considered on 28 Aug 2026 were declined, and why this one is not applied
   anywhere but here. Texture as a wink, not a redesign. */
/* ⚠️ THE GRADIENTS ARE SHARED WITH .spec — do not copy this block a second
   time. Extended 3 Sep 2026, on the owner's ask for more of this flavour.

   The rule above said this frame "is not applied anywhere but here", and the
   reason given was that the same texture on BODY COPY would be decoration.
   .spec is not body copy. It is the same instrument: on /work/ it prints
   version, live-since date, permission count and bytes sent, and on / it
   prints the status, the intended price and the shape of vault•sorter. Two
   readouts of identical structure were being drawn two different ways on two
   pages — a full grid of hairlines here, corner brackets there — which is the
   inconsistency, not the fix for it.

   So the restriction stands as written; .spec was simply always on the inside
   of it. Nothing new is invented, no third signifier is added, and the count
   of retro devices on this site is unchanged. */
.receipt,
.spec{
  --bracket:var(--ink-muted);
  --arm:11px;
  background:
    linear-gradient(var(--bracket),var(--bracket)) 0    0   /var(--arm) 1px no-repeat,
    linear-gradient(var(--bracket),var(--bracket)) 0    0   /1px var(--arm) no-repeat,
    linear-gradient(var(--bracket),var(--bracket)) 100% 0   /var(--arm) 1px no-repeat,
    linear-gradient(var(--bracket),var(--bracket)) 100% 0   /1px var(--arm) no-repeat,
    linear-gradient(var(--bracket),var(--bracket)) 0    100%/var(--arm) 1px no-repeat,
    linear-gradient(var(--bracket),var(--bracket)) 0    100%/1px var(--arm) no-repeat,
    linear-gradient(var(--bracket),var(--bracket)) 100% 100%/var(--arm) 1px no-repeat,
    linear-gradient(var(--bracket),var(--bracket)) 100% 100%/1px var(--arm) no-repeat;
}
.receipt{
  margin:20px 0 0;max-width:48ch;
  padding:14px 16px;
}
.receipt-row{
  display:grid;grid-template-columns:5.4rem 1fr;gap:14px;
  padding:9px 0;border-top:1px solid var(--rule-light);
}
/* The frame marks the top edge now, so the first row's own rule would read as
   a second line just inside the brackets. */
.receipt-row:first-child{border-top:0;padding-top:0}
.receipt-key{
  font:400 10.5px/1.7 var(--mono);letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-muted);
}
.receipt-val{font-size:14px;line-height:1.6;color:var(--ink-mid)}
.perms{display:flex;flex-wrap:wrap;gap:5px;margin:0;padding:0;list-style:none}
.perms li{
  font:400 11px/1 var(--mono);color:var(--ink-mid);
  border:1px solid var(--rule-light);padding:5px 7px;
}
.perms li.wide{color:var(--ink);border-color:var(--ink);font-weight:400}
.sends{font:400 16px/1.3 var(--mono);color:var(--sage)}

/* ── buttons ──────────────────────────────────────────────────────────── */
.btn{
  display:inline-block;text-align:center;text-decoration:none;white-space:nowrap;
  font:500 14px/1 var(--sans);
  background:var(--ink);color:var(--paper);
  padding:13px 22px;border-radius:999px;border:1px solid var(--ink);
}
.btn:hover{background:var(--sage-deep);border-color:var(--sage-deep);color:var(--paper)}
.btn-ghost{
  display:inline-block;text-align:center;text-decoration:none;white-space:nowrap;
  font:500 14px/1 var(--sans);color:var(--ink);
  padding:13px 22px;border-radius:999px;border:1px solid var(--ink);
  background:transparent;
}
.btn-ghost:hover{background:var(--ink);color:var(--paper)}
/* A PRESS THAT CAN BE FELT. Added 6 Sep 2026: until then .btn and .btn-ghost
   were the only interactive things on the site with no transition and no
   :active state at all — hover was an instant colour swap and a press was
   invisible, while .btn-quiet below has carried the full treatment since it
   was written.

   translateY(1px), NOT scale(.97). A 1px displacement reads as a physical
   surface taking pressure, which is the print/editorial register this site
   is in; a squish reads as a mobile game. 160ms on the transform because a
   button is a high-frequency target and 300ms would feel sluggish.

   Reduced motion loses both, following the .btn-quiet block below rather
   than inventing a second pattern for the same job. */
.btn,.btn-ghost{
  transition:background-color .18s ease,color .15s ease,
             border-color .18s ease,transform .16s cubic-bezier(.2,.7,.25,1);
}
.btn:active,.btn-ghost:active{transform:translateY(1px)}
@media(prefers-reduced-motion:reduce){
  .btn,.btn-ghost{transition:none}
  .btn:active,.btn-ghost:active{transform:none}
}
.btn-wait{
  display:inline-block;font:400 12px/1 var(--mono);letter-spacing:.06em;
  text-transform:uppercase;color:var(--ink-muted);
  padding:13px 16px;border:1px dashed var(--rule-light);border-radius:999px;
}
/* On a dark block the ink button would vanish. Gold fill, near-black type —
   7.77:1, and the only place gold is allowed to be a surface. */
.block .btn{background:var(--gold);border-color:var(--gold);color:var(--ink)}
.block .btn:hover{background:#DBBC63;border-color:#DBBC63;color:var(--ink)}

/* ══ THE QUIET STORE LINK ═════════════════════════════════════════════════
   Small, and after the shelf rather than beside the heading: the reader who
   has just been through six covers and their prices is the one who wants it.
   It is not a .btn — a second filled button in the same band would compete
   with "Add to Chrome" above it, and the store is not the primary action on
   this page. */
.shelf-store{margin:clamp(18px,2.4vw,26px) 0 0}
.btn-quiet{
  display:inline-block;
  font:400 12px/1 var(--mono);letter-spacing:.08em;
  padding:10px 15px;border:1px solid var(--rule-light);border-radius:2px;
  color:var(--ink-mid);text-decoration:none;
  transition:border-color .18s ease,color .18s ease,transform .18s ease;
}
.btn-quiet:hover{border-color:var(--ink-muted);color:var(--ink);transform:translateY(-1px)}
.btn-quiet:focus-visible{outline:2px solid var(--sage);outline-offset:3px}
/* On the dark planners band the paper hairline vanishes. */
.block .btn-quiet{border-color:rgba(255,255,255,.22);color:var(--on-dark-mid)}
.block .btn-quiet:hover{border-color:var(--gold);color:var(--on-dark)}
@media(prefers-reduced-motion:reduce){
  .btn-quiet{transition:none}.btn-quiet:hover{transform:none}
}

/* Work in progress reads as unfinished on purpose: the same record layout as
   a shipped tool, with the status doing the talking rather than a badge. */
/* ⚠️ 48ch, NOT 60ch — corrected 4 Sep 2026. .row-blurb is 48ch and .row-why
   sits directly under it in the same column, so at 60ch the two paragraphs
   justified to right edges 135px apart (R807 against R942). Ragged, nobody
   saw it; justified on 3 Sep, it read as two columns that had slipped.
   Justifying a block is what makes its measure visible — stacked justified
   paragraphs have to share an edge. Widening .row-blurb instead was the wrong
   way round: 48ch is the measure three extension rows already use. */
.row-why{margin:9px 0 0;color:var(--ink-muted);max-width:48ch}
.spec-wait{color:var(--amber-text,#8A6A1F)}

/* ══ THE LEDGER ═══════════════════════════════════════════════════════════
   The record stated in figures before either kind of work starts. It exists
   because /work/ opened "Three extensions on the Chrome Web Store" and framed
   a studio that also typesets 804 pages of planners as a three-extension shop.

   Read as a codec readout rather than as marketing stats: mono keys, tabular
   figures, hairline cells, no icons. Every number is counted from _data/ in
   the template, so none of it can go stale. */
.ledger{
  display:grid;gap:1px;margin:clamp(28px,3.6vw,40px) 0 0;
  background:var(--rule-light);
  border:1px solid var(--rule-light);
  grid-template-columns:repeat(2,1fr);
}
@media(min-width:40rem){.ledger{grid-template-columns:repeat(3,1fr)}}
@media(min-width:64rem){.ledger{grid-template-columns:repeat(5,1fr)}}
.ledger-cell{background:var(--paper);padding:14px 16px 15px}
.ledger dt{
  font:400 10px/1.4 var(--mono);letter-spacing:.14em;text-transform:uppercase;
  color:var(--ink-muted);
}
.ledger dd{
  /* ⚠️ 500, NOT 600 — CORRECTED 7 Sep 2026, AND IT WAS ALREADY WRONG.
     Geist ships here as 400 and 500 only (build_fonts.py instances two
     cuts). This asked for 600, so every figure in the /work/ ledger was
     being FAUX-BOLDED by the browser: synthetic emboldening smears the
     stems and is the exact defect MASTER_STRATEGY §5 records the Studio
     chrome being moved off Geist Mono to escape. Found while moving the
     display face to 400; it predates that change. */
  margin:7px 0 0;font:500 26px/1 var(--sans);letter-spacing:-.02em;
  color:var(--ink);font-variant-numeric:tabular-nums;
}
/* The qualifier rides with the figure rather than under it, so a cell is one
   phrase — "6 finished" — instead of a number needing a caption. */
.ledger dd i{
  font:400 11px/1 var(--mono);font-style:normal;letter-spacing:.08em;
  color:var(--ink-muted);margin-left:7px;
}
/* The zero is the whole argument, so it is the one cell that is not ink. */
.ledger-cell-good dd{color:var(--sage)}

/* ══ HUD CORNER BRACKETS ══════════════════════════════════════════════════
   Two L-shaped rules at opposite corners of a record row. Borrowed from the
   angular HUD framing of late-90s console interfaces, which is the right
   reference for a page whose whole content is a readout: version, date,
   permission count, bytes sent.

   Drawn with two pseudo-elements and four border edges — no asset, no SVG, no
   extra request. They sit at --rule-light and grow to --ink-muted on hover, so
   at rest they are almost subliminal and the row still reads as a plain
   record.

   ⚠️ NOT ON THE ROW ITSELF. .row already carries a border-top hairline; adding
   corners to that element makes a box, and a box is a card. The brackets are
   deliberately incomplete — they frame without enclosing. */
.hud{position:relative}
.hud::before,.hud::after{
  content:"";position:absolute;width:11px;height:11px;
  border:0 solid var(--rule-light);
  transition:border-color .2s ease,width .2s ease,height .2s ease;
  pointer-events:none;
}
.hud::before{top:-1px;left:-9px;border-top-width:1px;border-left-width:1px}
.hud::after{bottom:-1px;right:-9px;border-bottom-width:1px;border-right-width:1px}
.hud:hover::before,.hud:hover::after{
  border-color:var(--ink-muted);width:16px;height:16px;
}
@media(prefers-reduced-motion:reduce){
  .hud::before,.hud::after{transition:none}
  .hud:hover::before,.hud:hover::after{width:11px;height:11px}
}
/* Inside the gutter at narrow widths the brackets would clip. */
@media(max-width:40rem){.hud::before,.hud::after{display:none}}

/* ══ THE SCAN ═════════════════════════════════════════════════════════════
   A one-pixel horizontal rule every three pixels across the dark bands only.
   The CRT-era artefact, at an opacity where it reads as tooth in the black
   rather than as a filter over it — the ink looks pressed rather than printed.

   ⚠️ DARK GROUND ONLY, AND NEVER OVER TYPE. It is painted on an ::after that
   sits under the content (z-index 0 against the band's own stacking) so no
   glyph is ever crossed by a line. On the light paper the same texture reads
   as a rendering fault, which is why it is scoped to .block.
   No image, no request: a repeating-linear-gradient costs nothing. */
.block{position:relative;isolation:isolate}
.block::after{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:repeating-linear-gradient(
    to bottom,
    rgba(255,255,255,.028) 0 1px,
    transparent 1px 3px
  );
}
/* Forced-colors strips the gradient anyway; say so rather than leave a rule
   that silently does nothing. */
@media(forced-colors:active){.block::after{display:none}}

/* ── the shelf: six across, no scrollbar ──────────────────────────────── */
.shelf{display:grid;gap:clamp(12px,1.7vw,22px);grid-template-columns:repeat(2,1fr)}
@media(min-width:34rem){.shelf{grid-template-columns:repeat(3,1fr)}}
@media(min-width:62rem){.shelf{grid-template-columns:repeat(6,1fr)}}
/* ⚠️ THE CAPTION IS PART OF THE LINK, from 7 Sep 2026, and it is done with a
   pseudo-element rather than by moving markup. "buy on Payhip" sits in the
   <figcaption>, outside <a class="cover-buy">, so the only text on the shelf
   that reads as a buy affordance was inert — six dead clicks at the page's
   peak moment.
   <figcaption> must stay a direct child of <figure>, so it cannot simply move
   inside the anchor. Wrapping the caption in a SECOND anchor would put two
   links to one destination on every cover, which is worse for a screen reader
   than the problem it fixes. A stretched ::after keeps exactly one link and
   changes not one pixel. */
.cover{margin:0;position:relative}
.cover-buy::after{content:"";position:absolute;inset:0;z-index:1}
.cover img{
  display:block;width:100%;aspect-ratio:.707;object-fit:cover;
  box-shadow:0 10px 26px rgba(26,26,26,.20);
  transition:transform .18s cubic-bezier(.2,.7,.25,1);
}
.cover:hover img{transform:translateY(-7px)}
@media(prefers-reduced-motion:reduce){
  .cover img{transition:none}.cover:hover img{transform:none}
}
.cover figcaption{
  margin-top:11px;font:400 10.5px/1.55 var(--mono);letter-spacing:.04em;
  color:var(--ink-muted);
}
.cover .cover-title{display:block;color:var(--ink-mid)}

/* ── whether a planner can be bought yet ──────────────────────────────────
   The state line sits under the page count, in the same mono as every other
   receipt on this site, so "on sale" reads as a fact about the product rather
   than as a sales badge. Six identical "Coming soon" lines would be noise, so
   the live one is the one that changes colour - the exception is marked, not
   the rule. */
.cover-state{display:block;margin-top:4px;color:var(--ink-muted)}
.cover-state-live{color:var(--sage)}
.block .cover-state{color:var(--on-dark-mid)}

/* A buyable cover is a link. The hover lift already lives on `.cover img`, so
   the anchor adds no motion of its own — it only has to not break the layout
   and to show a focus ring that clears the artwork. */
.cover-buy{display:block;border-radius:2px}
.cover-buy:focus-visible{outline:2px solid var(--sage);outline-offset:4px}
/* The image carries a shadow; on dark ground the default link underline would
   cut across the cover. */
.cover-buy,.cover-buy:hover{text-decoration:none}

.shelf-note-tight{margin-top:10px;padding-top:0;border-top:0}
.block .cover figcaption{color:var(--on-dark-mid)}
.block .cover .cover-title{color:var(--on-dark)}

.shelf-note{
  margin:clamp(24px,3.4vw,36px) 0 0;padding-top:18px;
  border-top:1px solid var(--rule-light);
  font:400 13px/1.7 var(--mono);letter-spacing:.03em;color:var(--ink-muted);
  font-variant-numeric:tabular-nums;
}
.block .shelf-note{border-top-color:rgba(201,168,76,.42);color:var(--on-dark-mid)}

/* ── prose ────────────────────────────────────────────────────────────── */
.prose{max-width:var(--measure)}
.prose > :first-child{margin-top:0}
/* Searched before adding: there was no adjacency rule anywhere in this file
   (3 Sep 2026), and `.prose > :first-child{margin-top:0}` above is exactly
   what removes the gap when a lead sits beside prose rather than inside it.
   The value matches `.section-note`'s own margin-top so the two things that
   can follow a `.section-lead` sit at the same distance from it. */
.section-lead + .prose{margin-top:16px}
.prose h2{
  font:400 clamp(20px,2.2vw,26px)/1.2 var(--display);letter-spacing:-.02em;
  margin:38px 0 12px;
}
.prose p{margin:0 0 19px;color:var(--ink-mid);line-height:1.72;text-wrap:pretty}
.prose p:last-child{margin-bottom:0}
.prose strong{color:var(--ink);font-weight:500}
.prose ul{margin:0 0 19px;padding-left:18px;color:var(--ink-mid)}
.prose li{margin:0 0 7px}

/* ── facts ────────────────────────────────────────────────────────────── */
.facts{
  display:flex;flex-direction:column;max-width:46ch;margin:0;
  font:400 13px/1.6 var(--mono);letter-spacing:.03em;color:var(--ink-mid);
}
.facts > div{
  display:flex;justify-content:space-between;gap:16px;
  padding:13px 0;border-top:1px solid var(--rule-light);
}
.facts > div:last-child{border-bottom:1px solid var(--rule-light)}
.facts dt{color:var(--ink-muted);margin:0}
.facts dd{margin:0;text-align:right;color:var(--ink)}

/* ── forms ────────────────────────────────────────────────────────────── */
.signup{max-width:var(--measure)}
.signup p{color:var(--ink-mid);line-height:1.7;margin:0 0 20px}
.signup-form{display:flex;gap:10px;flex-wrap:wrap;margin:0 0 14px}
.signup-form input[type=email]{flex:1 1 15rem;min-width:0}
.signup-note{font-size:14px;line-height:1.65;color:var(--ink-muted);margin:0;max-width:56ch}

.form-stack{display:flex;flex-direction:column;gap:4px;margin:0 0 20px;max-width:44ch}
.form-stack label{
  font:400 10.5px/1.7 var(--mono);letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-muted);margin-top:18px;
}
.form-stack label:first-of-type{margin-top:0}
.field,.signup-form input[type=email]{
  font:400 16px/1.5 var(--sans);color:var(--ink);
  background:#FFFFFF;border:1px solid var(--rule-light);
  border-radius:3px;padding:12px 14px;width:100%;
}
.field:focus,.signup-form input[type=email]:focus{border-color:var(--ink)}
.field::placeholder{color:var(--ink-muted)}
textarea.field{resize:vertical;min-height:8.5rem}
.form-stack button,.signup-form button{
  cursor:pointer;font:500 14px/1 var(--sans);
  background:var(--ink);color:var(--paper);border:1px solid var(--ink);
  border-radius:999px;padding:13px 24px;
}
.form-stack button{align-self:flex-start;margin-top:20px}
.form-stack button:hover,.signup-form button:hover{
  background:var(--sage-deep);border-color:var(--sage-deep);
}
/* Netlify's spam honeypot. Hidden from people, reachable by a bot, and it must
   NOT use display:none — some bots skip those. */
.hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}

/* ── journal ──────────────────────────────────────────────────────────── */
.entries{list-style:none;margin:0;padding:0;border-top:1px solid var(--rule)}
.entries li{padding:26px 0;border-bottom:1px solid var(--rule-light)}
.entry-date,
.entry-meta{font:400 11px/1 var(--mono);letter-spacing:.14em;color:var(--ink-muted)}
.entry-sep{opacity:.5;margin:0 .4em}
.entry-title{
  font:400 clamp(21px,2.2vw,27px)/1.15 var(--display);letter-spacing:-.024em;
  margin:10px 0 8px;
}
.entry-title a{color:var(--ink);text-decoration:none}
.entry-title a:hover{color:var(--sage-deep)}
.entries p{margin:0;color:var(--ink-mid);max-width:52ch}

/* The topic row is built by journal.js and is absent without it — a control
   that cannot work should not be drawn. See the header of that file. */
.topic-row{
  display:flex;flex-wrap:wrap;gap:6px;
  margin:0 0 22px;
}
.topic{
  font:400 11px/1 var(--mono);letter-spacing:.1em;text-transform:lowercase;
  color:var(--ink-mid);background:transparent;
  border:1px solid var(--rule-light);border-radius:999px;
  padding:8px 13px;cursor:pointer;
  transition:border-color .15s ease,color .15s ease,background-color .15s ease;
}
.topic:hover{border-color:var(--ink-muted);color:var(--ink)}
.topic[aria-pressed="true"]{
  background:var(--ink);border-color:var(--ink);color:var(--paper);
}
.topic-n{opacity:.55;margin-left:.35em}
.topic[aria-pressed="true"] .topic-n{opacity:.7}
@media(prefers-reduced-motion:reduce){.topic{transition:none}}

.entry-topics{margin:12px 0 0;display:flex;flex-wrap:wrap;gap:6px}
.entry-topic{
  font:400 10px/1 var(--mono);letter-spacing:.1em;
  color:var(--ink-muted);background:var(--paper-sunk);
  padding:5px 8px;border-radius:3px;
}

.empty{border:1px solid var(--rule-light);padding:clamp(26px,4vw,44px);max-width:var(--measure)}
.empty p{margin:14px 0 0;color:var(--ink-muted);line-height:1.7}
.noscript{border:1px solid var(--rule-light);padding:24px;max-width:var(--measure)}
.noscript p{margin:0;color:var(--ink-mid)}

/* ── footer ───────────────────────────────────────────────────────────── */
.site-footer{background:var(--dark);color:var(--on-dark-mid);margin-top:auto}
.footer-inner{
  max-width:var(--shell);margin:0 auto;
  padding:clamp(40px,5.5vw,72px) var(--gutter) clamp(44px,6vw,80px);
  display:flex;flex-wrap:wrap;gap:clamp(28px,4.5vw,64px);
}
.footer-about{flex:1 1 280px}
.footer-name{
  margin:0 0 10px;font:400 20px/1 var(--display);letter-spacing:-.02em;
  color:var(--on-dark);
}
.footer-about p{margin:0;max-width:34ch;font-size:14px;line-height:1.65;text-wrap:pretty}
.footer-links{display:flex;flex-direction:column;gap:10px;flex:0 0 auto;font-size:14px}
.footer-links a{color:var(--on-dark-mid);text-decoration:none}
.footer-links a:hover{color:var(--gold)}
.footer-note{
  flex:1 1 100%;margin:0;padding-top:22px;
  border-top:1px solid rgba(201,168,76,.42);
  font:400 11px/1.8 var(--mono);letter-spacing:.04em;color:var(--on-dark-mid);
}

/* ── one entrance, nothing else moves ─────────────────────────────────────
   CSS only. No GSAP, no WebGL, no scroll-pinned chapters — a site arguing for
   reliability cannot ship motion that breaks on Safari. */
@media(prefers-reduced-motion:no-preference){
  .rise{animation:rise .62s cubic-bezier(.2,.7,.25,1) backwards}
  .rise-1{animation-delay:.06s}
  .rise-2{animation-delay:.13s}
  .rise-3{animation-delay:.2s}
  @keyframes rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
}

/* The menu drops, and the five destinations settle after it. Same easing and
   the same `backwards` fill as .rise above, so the one entrance this site has
   is the one the menu uses too.

   ⚠️ SHORT ON PURPOSE — 160ms for the panel, items landing by 400ms. The
   masthead comment above already makes this argument for the nav pill: this is
   a high-frequency target, and slow is worse than abrupt on something a reader
   opens to get somewhere else. This is not a hero.

   ⚠️ ONLY THE OPENING IS ANIMATED, and that is inherent rather than an
   oversight. Without JavaScript a closing <details> removes its content
   immediately, so there is no frame in which to animate it out. The trade was
   accepted when the burger was built on <details> instead of a click handler.

   ⚠️ FIVE DELAYS FOR site.nav's FIVE ITEMS. A sixth would simply arrive with
   the fifth - no delay, still animated, nothing broken. Add a line if the nav
   grows. */
@media(prefers-reduced-motion:no-preference) and (max-width:56rem){
  .nav-fold[open] ~ .nav-pill{
    animation:nav-drop .16s cubic-bezier(.2,.7,.25,1);
  }
  .nav-fold[open] ~ .nav-pill a{
    animation:nav-item .26s cubic-bezier(.2,.7,.25,1) backwards;
  }
  .nav-fold[open] ~ .nav-pill a:nth-child(1){animation-delay:.04s}
  .nav-fold[open] ~ .nav-pill a:nth-child(2){animation-delay:.08s}
  .nav-fold[open] ~ .nav-pill a:nth-child(3){animation-delay:.12s}
  .nav-fold[open] ~ .nav-pill a:nth-child(4){animation-delay:.16s}
  .nav-fold[open] ~ .nav-pill a:nth-child(5){animation-delay:.20s}
  @keyframes nav-drop{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:none}}
  @keyframes nav-item{from{opacity:0;transform:translateY(-7px)}to{opacity:1;transform:none}}
}

/* ── the sage dot ─────────────────────────────────────────────────────────
   Product names are `lowercase•two-words`. The dot is the part of the name
   that is brand rather than word, so it takes the accent and the words stay
   ink. Applied by the `bullet` filter, never hand-written into the data. */
.dot{color:var(--sage);display:inline-block}
/* ⚠️ THE DOT TAKES GOLD ON EVERY DARK GROUND, and .demo was missing from
   this list until 7 Sep 2026. Sage #547357 on the panel's #0A0A0A is
   1.9:1 — the brand mark inside every product name in every demo header
   was effectively invisible. Gold on that ground is 8.9:1. */
.block .dot,.site-footer .dot{color:var(--gold)}
.demo .dot{color:var(--dm-accent)}

/* The dot breathes. It is the one piece of the name that is brand rather than
   word, so it is the one thing allowed to move on its own. Slow and shallow:
   at 2.6s and a third of a stop it reads as alive rather than as a
   notification badge demanding to be clicked. */
@media(prefers-reduced-motion:no-preference){
  .dot{animation:pulse 2.6s ease-in-out infinite}
  .row-name .dot{animation-delay:calc(var(--i,0) * .5s)}
  @keyframes pulse{
    0%,100%{opacity:1;transform:scale(1)}
    50%{opacity:.42;transform:scale(.86)}
  }
}

/* ── figures ──────────────────────────────────────────────────────────────
   Replaces a mono definition list with a rule between every row, which read
   as the spec sheet for a fridge. The number is the thing worth seeing, so
   the number is what is set large. */
.figures{
  display:grid;gap:clamp(22px,3.5vw,44px);margin:0;
  grid-template-columns:repeat(2,1fr);
}
@media(min-width:48rem){.figures{grid-template-columns:repeat(4,1fr)}}
.figures > div{
  display:flex;flex-direction:column;gap:10px;
  padding-top:16px;border-top:2px solid var(--ink);
}
.figures dd{
  margin:0;font:400 clamp(38px,5.5vw,64px)/.9 var(--display);
  letter-spacing:-.04em;color:var(--ink);font-variant-numeric:tabular-nums;
}
.figure-of{font-size:.45em;color:var(--ink-muted);letter-spacing:-.01em}
/* ∞ is drawn much wider and shorter than a digit, so at the figure size it
   sits small and low against the numerals beside it. Nudged up rather than
   set larger, which would make it shout. */
.figure-inf{font-size:.9em;line-height:1;transform:translateY(-.06em)}
.figures dt{
  margin:0;font:400 11px/1.5 var(--mono);letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-muted);
}
/* The as-at on a measured figure. Quieter than the label and NOT uppercase,
   so it reads as a qualifier rather than a second label. Only the installs
   figure carries one, because it is the only figure on the page that was
   measured rather than counted — see the comment in about.njk. */
.figure-asat{
  display:block;margin-top:5px;
  letter-spacing:.04em;text-transform:none;
  color:var(--ink-muted);opacity:.8;
}

/* ── social ───────────────────────────────────────────────────────────── */
.social{display:flex;gap:18px;list-style:none;margin:22px 0 0;padding:0}
.social a{
  display:inline-flex;align-items:center;gap:8px;text-decoration:none;
  color:var(--on-dark-mid);font-size:13px;
}
.social a:hover{color:var(--gold)}
.social svg{
  width:19px;height:19px;flex:none;
  fill:none;stroke:currentColor;stroke-width:1.6;
  stroke-linecap:round;stroke-linejoin:round;
}
.social svg .fill{fill:currentColor;stroke:none}

/* Visually hidden, still announced. Not `display:none`, which removes it from
   the accessibility tree along with everything else. */
.vh{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
}

/* ── the typing labels ────────────────────────────────────────────────────
   Each extension's category types itself in as it scrolls into view. The text
   is in the HTML and is styled visible by default — the script opts IN to
   animating, so with JavaScript off, or before the observer fires, the label
   is simply there. A CSS-first reveal that starts at opacity 0 would leave
   the text invisible forever if the script failed, which is the trade this
   avoids. */
/* pre-wrap is what lets the spaces stay ordinary spaces in type-in.js: it
   stops one collapsing while its character is hidden — the jitter the script
   used a non-breaking space for — while still allowing the line to BREAK at
   it. A non-breaking space did the first job and silently forbade the second,
   which put the 44-character status line 73px outside its column at 320px. */
.row-does,
.hero-status{white-space:pre-wrap}
.row-does [data-ch],
.hero-status [data-ch]{opacity:0}
.row-does.is-typing [data-ch],
.hero-status.is-typing [data-ch]{animation:ch .01s linear forwards}
@keyframes ch{to{opacity:1}}
@media(prefers-reduced-motion:reduce){
  .row-does [data-ch],
  .hero-status [data-ch]{opacity:1;animation:none}
}

/* ── inline code ──────────────────────────────────────────────────────────
   Added 31 Aug 2026. The journal posts name real permissions and real APIs —
   `storage`, `alarms`, `unlimitedStorage`, `sendBeacon` — and every one of
   them was rendering as plain body text, indistinguishable from prose. A post
   arguing about which permissions an extension declares is unreadable if the
   permissions do not look like permissions.

   The tint is `--paper-sunk`, the same one the entry topic chips and the form
   fields use. No border and no syntax colour: this is a token in a sentence,
   not a code block, and a box around a single word interrupts the line it is
   in. `word-break` because permission names are long and unhyphenatable, and
   the prose around them is justified. */
code{
  font:400 .88em/1.4 var(--mono);
  background:var(--paper-sunk);
  padding:.15em .4em;border-radius:3px;
  color:var(--ink);
  word-break:break-word;
}
.block code{background:rgba(255,255,255,.09);color:var(--on-dark)}
a code{color:inherit}
/* A code block, if one is ever written. Scrolls inside itself rather than
   widening the page — the 320px overflow at line 217 came from exactly this
   kind of unbreakable content. */
pre{
  margin:clamp(20px,2.6vw,28px) 0;
  padding:clamp(14px,2vw,18px);
  background:var(--dark);color:var(--on-dark-mid);
  border-radius:8px;overflow-x:auto;
  font:400 13px/1.7 var(--mono);
}
pre code{background:none;padding:0;color:inherit;word-break:normal}

/* ── a journal post ───────────────────────────────────────────────────────
   Indented to sit under the section-head rail, so a post lines up with every
   other page rather than starting hard against the gutter. */
.post-body{max-width:62ch}
@media(min-width:52rem){.post-body{margin-left:calc(var(--col-label) + clamp(14px,2.5vw,40px))}}
.post-body em{font-style:italic}

/* ── justified setting ────────────────────────────────────────────────────
   Justified needs hyphenation or it opens rivers of white space down the
   column. `hyphens:auto` works here because <html lang="en-IE"> is set - it
   is silently inert without a language. Applied to running prose only: mono
   metadata, labels and short one-line blurbs stay ragged, where justification
   would stretch four words across a full measure. */
.prose p,
.post-body p,
.post-body li,
.block-body,
.hero-lead,
/* Added 3 Sep 2026, owner's call to justify the prose more widely. Both are
   17px Geist running text, which is what this block is for.
   ⚠️ A SECOND, NEAR-DUPLICATE BLOCK OF THIS WAS WRITTEN HIGHER UP THE FILE
   BEFORE THIS ONE WAS NOTICED, and it also listed .section-lead and
   .block-lead — 38px and 60px Archivo, display headings in everything but tag
   name. "Six print-ready PDFs for 2027." justified over two lines stretched
   the first line's word spacing to fill the measure. A class called "lead" is
   not necessarily prose; check the computed font size before adding one. */
.page-lead,
.row-blurb,
.row-why,
/* Added 4 Sep 2026, owner's call, after measuring every multi-line text block
   on six pages rather than guessing which ones looked odd. These three were
   the only sans running prose left ragged; everything else still unjustified
   is mono metadata (.section-note, .footer-note, .demo-hint, .shelf-note,
   .hero-status, .pp-note) which this block has always deliberately excluded.
     .footer-about p  14px, 2 lines, on EVERY page - the most repeated
                      inconsistency on the site and the easiest to miss
     .entry-desc      17px, 3 lines, the journal index
     .signup-note     14px, 3 lines, under the email form
   ⚠️ `ul.entries li` is NOT here although it measures 8 lines. That li wraps
   the date, the title and the description; justifying it would justify the
   heading inside it. The prose within it is .entry-desc, which is. */
.footer-about p,
.entry-desc,
.signup-note,
/* ⚠️ THE SENTENCE ABOVE SAYING "THESE THREE WERE THE ONLY SANS RUNNING PROSE
   LEFT RAGGED" WAS WRONG, and .receipt-val is the counter-example — added
   4 Sep 2026 after the owner looked at the Software section and counted three
   alignments in it. The sweep that found the other three looked for multi-line
   PARAGRAPHS; this one is a <span> in a grid cell, so it was never in the set
   being read. It is Geist sans 14px over 409px running to 2 and 3 lines, which
   is prose by every test the block applies. The exclusion rule is MONO
   metadata, and this was never mono.
   Cost measured before adding it: worst line 1.056, most 1.005-1.027, against
   a site 90th percentile of 1.087. It renders only on the homepage, in three
   extension receipts and two software ones, and all five improve. */
.receipt-val{
  text-align:justify;
  hyphens:auto;
  -webkit-hyphens:auto;
  text-wrap:pretty;
}
/* ⚠️ NOT ON A NARROW MEASURE. Below ~34rem the column is short enough that
   even hyphenated justification opens visible gaps, so phones get the ragged
   edge they read better anyway. */
@media(max-width:34rem){
  .prose p,.post-body p,.post-body li,.block-body,
  .hero-lead,.page-lead,.row-blurb,.row-why,
  .footer-about p,.entry-desc,.signup-note,.receipt-val{
    text-align:left;hyphens:manual;
  }
}

/* ── /about/: where ───────────────────────────────────────────────────────
   The county outline, generated by tools/build_map.py from the real OSM
   boundary. Line only — no fill, no terrain, no labels. A filled county would
   be a graphic; an outline with one marked point is a statement of fact, which
   is the register the rest of the page is in.

   The stroke is `--ink-muted`, not `--ink` and not `--rule-light`. Near-black
   at 260 points reads as a blot at this size; `--rule-light` was tried first
   and disappeared into the paper entirely — the county has to be legible
   before the marker on it means anything. */
.place{display:grid;gap:clamp(24px,4vw,52px);align-items:start}
@media(min-width:52rem){.place{grid-template-columns:minmax(0,260px) 1fr}}

.place-map{max-width:260px}
.map{display:block;width:100%;height:auto;overflow:visible}
.map-outline{
  fill:none;stroke:var(--ink-muted);stroke-width:1.1;opacity:.75;
  stroke-linejoin:round;stroke-linecap:round;
  /* vector-effect keeps the hairline a hairline when the SVG is scaled down,
     rather than thinning to invisibility with the viewBox. */
  vector-effect:non-scaling-stroke;
}
.map-home-dot{fill:var(--sage)}
.map-home-halo{fill:none;stroke:var(--sage);stroke-width:1;opacity:.35}

/* One breath, once, when it scrolls in — the marker settling rather than
   pulsing forever. `Almost nothing loops` is the motion budget, and a beacon
   blinking under a paragraph about being left alone would argue against the
   paragraph. */
@media(prefers-reduced-motion:no-preference){
  .map-home-halo{
    transform-origin:center;
    animation:map-settle 1.4s cubic-bezier(.22,.61,.36,1) .25s 1 both;
  }
}
@keyframes map-settle{
  from{transform:scale(.35);opacity:0}
  60%{opacity:.5}
  to{transform:scale(1);opacity:.35}
}

.place-copy{max-width:52ch;display:flex;flex-direction:column;justify-content:center}
/* One sentence, so it is set as a statement rather than as a paragraph. It
   sits at display size because it is the only line in the column and a single
   body-sized sentence beside a map reads as a stray caption. */
.place-line{
  margin:0;
  font:400 clamp(24px,3.2vw,40px)/1.15 var(--display);
  letter-spacing:-.026em;color:var(--ink);text-wrap:balance;
}
.place-credit{
  margin-top:clamp(18px,2.4vw,26px);
  font:400 11px/1.6 var(--mono);letter-spacing:.06em;
  color:var(--ink-muted);
}
/* Not justified: .prose justifies running text, and a two-line mono credit
   stretched across a full measure opens rivers immediately. */
.place-credit{text-align:left;hyphens:none}
.place-code{color:var(--ink-mid)}

/* ── /work/: the record ───────────────────────────────────────────────────
   Added 31 Aug 2026, when /work/ stopped being the homepage with the receipts
   removed. The homepage argues; this page states. So the furniture here is a
   spec list and a lead paragraph, not a second set of demos.

   No new colours, no new type roles — everything below uses tokens and faces
   already declared. A page earns its place by carrying different FACTS, not by
   looking different. */
.page-lead{
  margin:18px 0 0;max-width:52ch;
  color:var(--ink-mid);font-size:17px;line-height:1.7;text-wrap:pretty;
}

/* The shipping record. A definition list rather than a table: four unrelated
   facts about one thing, which is what a dl is for, and it collapses to one
   column at narrow widths without any of a table's horizontal-scroll problem. */
.spec{
  margin:18px 0 0;max-width:48ch;
  display:grid;gap:0;
  /* ⚠️ NO border-top, AND NO border-bottom ON THE LAST ROW. Both were here
     until 3 Sep 2026 and both have to go now the corner brackets do the outer
     edge: a full hairline plus brackets is a box, and the .hud comment above
     says why a box is wrong here — it makes a record read as a card. The
     brackets frame without enclosing, which only works if nothing else is
     enclosing. Padding matches .receipt so the two readouts sit identically
     inside their frames. */
  padding:14px 16px;
}
.spec > div{
  display:grid;grid-template-columns:9rem 1fr;gap:14px;
  padding:9px 0;border-bottom:1px solid var(--rule-light);
}
.spec > div:first-child{padding-top:0}
.spec > div:last-child{border-bottom:0;padding-bottom:0}
.spec dt{
  margin:0;font:400 11px/1.5 var(--mono);letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-muted);
}
.spec dd{margin:0;font:400 14px/1.5 var(--mono);color:var(--ink)}
/* "nothing" is the strongest word on the page, so it takes the accent — the
   same treatment .sends gets in the receipt, for the same reason. */
.spec-good{color:var(--sage)}
@media(max-width:34rem){
  .spec > div{grid-template-columns:1fr;gap:2px}
}

/* The strapline, in the third column where the extension rows put the button.
   Right-aligned at width so the column reads as a caption rather than a second
   paragraph competing with the buyer sentence beside it. */
.row-strap{
  margin:0;color:var(--ink-muted);font-style:italic;
  line-height:1.55;text-wrap:pretty;
}
/* 200px, not the 150px the extension rows use. That column was sized for
   `page•pin` and `tab•tally`; "Business Planner" and "Self Care Planner" are
   twice the width and broke onto three lines in it. The rail stays visually
   aligned with the rows above because both are set from the same left edge —
   only the split moves. */
@media(min-width:52rem){
  .row-for{grid-template-columns:200px 1fr 220px}
  .row-strap{text-align:right}
}

/* ── the tool demos ───────────────────────────────────────────────────────
   Added 31 Aug 2026. Each extension row carries a small working likeness of
   the tool above its receipt: the demo answers "what is this", the receipt
   answers "what does it cost me", and a stranger asks the first one first.

   THE APP PALETTE, NOT THE PAGE'S, and not a third one. These reuse the same
   scoped tokens the hero's page•pin panel declares at .pp — DECISIONS.md
   2026-08-28 ruled that a thing which IS an app takes the app identity, and a
   picture of an extension is the same argument. Declared once here rather than
   copied per demo, so there is one dark in the stylesheet and not four.

   The markup is written in its FINISHED state and demos.js winds it back.
   With no JavaScript a visitor sees a correct still of the tool instead of an
   empty frame. Reasoning in _includes/tool-demo.njk.

   ⚠️ NO CSS-ONLY AUTOPLAY. Nothing here animates from a stylesheet rule alone.
   If it did, a reduced-motion visitor would need a second rule to switch it
   off and the "honoured by absence" guarantee would become "honoured by
   remembering". demos.js declines to start; the CSS never starts on its own. */
.demo{
  --pp-bg:#111111;--pp-surface:#1a1a1a;
  --pp-border:#2e2e2e;--pp-border-hi:#3d3d3d;
  --pp-ink-1:#f0ede8;--pp-ink-2:#a09c97;--pp-ink-3:#8a8681;  /* raised — see above */
  --pp-accent:#7a9e7e;

  margin:20px 0 0;max-width:48ch;
  background:var(--pp-bg);
  border:1px solid var(--pp-border);
  border-radius:12px;
  overflow:hidden;
  color:var(--pp-ink-2);
  /* ⚠️ THE MONO SIZE GOES ON THE CHILDREN, NOT HERE, AND THAT IS THE POINT.
     `ch` resolves against the element's OWN font. Setting 11px mono on .demo
     made its 48ch measure 317px while .row-blurb and .receipt — same 48ch, but
     in the body face — measured 541px, so the panel sat visibly narrow under
     copy it was meant to line up with. Leaving .demo in the inherited face
     makes all three agree by construction rather than by a matching number
     somebody has to maintain. */
  font-size:inherit;
  /* min-width:0 for the same reason .hero-grid needs it: a grid child with
     fixed-ish content blows the column out at 320px otherwise. */
  min-width:0;
}
.demo-chrome{
  display:flex;align-items:center;gap:8px;
  padding:9px 12px;
  border-bottom:1px solid var(--pp-border);
  background:var(--pp-surface);
  color:var(--pp-ink-3);
  font:400 10px/1.5 var(--mono);
  letter-spacing:.06em;text-transform:uppercase;
}
.demo-dot{
  width:7px;height:7px;border-radius:50%;
  background:var(--pp-accent);flex:none;
}
.demo-body{
  padding:14px 12px;min-height:118px;
  font:400 11px/1.5 var(--mono);
}
.demo-body-centre{display:grid;place-content:center;text-align:center;gap:6px}

/* ── vault•sorter's panel ──────────────────────────────────────────────────
   Shares .demo, .demo-chrome and .demo-hint with the extension panels; only
   the body differs, because only the body is a different product. Reasoning
   and the identity ruling are in _includes/software-demo.njk. */
.demo-theme{
  margin-left:auto;display:flex;border:1px solid var(--pp-border-hi);
  border-radius:999px;overflow:hidden;
}
.demo-theme-opt{padding:2px 9px;color:var(--pp-ink-3)}
.demo-theme-opt.is-on{background:var(--pp-surface);color:var(--pp-ink-1)}

.demo-verdicts{list-style:none;margin:0;padding:0;display:grid;gap:9px}
.demo-verdicts li{
  display:grid;grid-template-columns:auto auto 1fr;gap:10px;align-items:center;
  /* Wound back by demos.js, which removes data-pending. No CSS autoplay. */
  transition:opacity 260ms cubic-bezier(0.22,1,0.36,1),
             transform 260ms cubic-bezier(0.22,1,0.36,1);
}
.demo-verdicts li[data-pending]{opacity:0;transform:translateY(4px)}
.demo-isin{color:var(--pp-ink-1);letter-spacing:.02em}
.demo-tick{color:var(--pp-ink-3)}
.demo-pill{
  justify-self:end;padding:1px 7px;border-radius:3px;
  border:1px solid var(--pp-border-hi);color:var(--pp-ink-2);
  font-size:9.5px;letter-spacing:.1em;white-space:nowrap;
}
.demo-pill-fund{border-color:var(--pp-accent);color:var(--pp-accent)}
/* Amber for the refusal, never red — the same rule the product itself follows.
   A refusal is a correct answer, and red would read as a failure. */
.demo-pill-unk{border-color:#9E651A;color:#E8943A}

.demo-charge{
  display:flex;align-items:baseline;gap:10px;
  margin:14px 0 0;padding:10px 0 0;border-top:1px solid var(--pp-border);
  transition:opacity 260ms cubic-bezier(0.22,1,0.36,1);
}
.demo-charge[data-pending]{opacity:0}
.demo-charge-k{color:var(--pp-ink-3)}
.demo-charge-v{
  margin-left:auto;color:var(--pp-ink-1);font-size:15px;letter-spacing:-.01em;
  font-variant-numeric:tabular-nums;
}
.demo-hint{
  margin:0;padding:9px 12px;
  border-top:1px solid var(--pp-border);
  background:var(--pp-surface);
  color:var(--pp-ink-3);
  /* ⚠️ 11px, NOT 10px, from 7 Sep 2026, AND THE REASON IS THE LINE ITSELF.
     This is the only prose in the panel — "Illustrative, not your data",
     "a holding it cannot place is refused, not guessed". It was 10px inside an
     aria-hidden subtree, so it was both the smallest text in the panel and
     unreachable by a screen reader. The subtree fix made it reachable; this
     makes it readable. One pixel, on four panels, on the disclaimer a
     suspicious reader is looking for. */
  font:400 11px/1.5 var(--mono);
  letter-spacing:.04em;
}
.demo-hint b{color:var(--pp-ink-2);font-weight:400}

/* page•pin — grey bars stand in for body text, so the note is the only thing
   with words on it and the eye goes there. Not lorem: fake sentences invite
   reading, and there is nothing to read. */
.demo-body[data-pin-surface]{position:relative}
.demo-line{height:6px;border-radius:3px;background:var(--pp-border);margin:0 0 9px}
.demo-line-w1{width:92%}
.demo-line-w2{width:78%}
.demo-line-w3{width:85%}
.demo-note{
  background:var(--pp-surface);
  border:1px solid var(--pp-border-hi);
  border-left:2px solid var(--pp-accent);
  border-radius:6px;
  padding:8px 10px;margin:0 0 9px;
  display:flex;justify-content:space-between;gap:10px;align-items:baseline;
  transition:opacity .28s ease,transform .28s ease;
}
.demo-note-text{color:var(--pp-ink-1)}
.demo-note-meta{color:var(--pp-accent);font-size:10px;flex:none}
/* Wound back by demos.js before it plays the note in. Never set from CSS
   alone — see the autoplay note above. */
.demo-note[data-pending]{opacity:0;transform:translateY(-4px)}

/* site•stay — bars grow from the left. width is set inline by demos.js from
   the data-bar value, so the number lives in the markup and not in two places. */
.demo-bars{list-style:none;margin:0;padding:0;display:grid;gap:11px}
.demo-bars li{display:grid;grid-template-columns:44px 1fr;gap:10px;align-items:center}
.demo-bar-label{color:var(--pp-ink-3);letter-spacing:.06em}
.demo-bar{display:block;height:8px;border-radius:4px;background:var(--pp-border);overflow:hidden}
.demo-bar i{
  display:block;height:100%;border-radius:4px;
  background:var(--pp-accent);
  transition:width .9s cubic-bezier(.22,.61,.36,1);
}

/* tab•tally — the number is the product, so it gets the display face. */
.demo-count{
  margin:0;font:400 40px/1 var(--display);
  color:var(--pp-ink-1);font-variant-numeric:tabular-nums;letter-spacing:-.02em;
}
.demo-notice{margin:0;color:var(--pp-accent);transition:opacity .35s ease}
.demo-notice[data-pending]{opacity:0}

/* ══ THE WORKING DEMOS ═════════════════════════════════════════════════════
   Rebuilt 7 September 2026 from the Claude Design comp the owner approved.

   WHAT CHANGED, AND WHY IT IS NOT DECORATION. The old panels were pictures:
   written in their finished state and wound backwards by script. These are
   the same idea taken one step further — they RUN. page•pin filters on what
   you type and pins what you write; vault•sorter streams its verdicts in and
   can be re-run. A tool whose whole pitch is "it does one thing" is better
   argued by letting a stranger do that one thing than by describing it.

   ⚠️ THE STILL-STATE RULE SURVIVES AND IS NOT NEGOTIABLE. Every panel ships
   in its FINISHED state in the markup; assets/js/demos.js winds it back and
   plays it forward. JavaScript off, blocked, or still loading shows a correct
   picture rather than an empty frame. Anything added here follows that or it
   does not ship.

   ⚠️ ROUNDED CORNERS ARE A KEEP, NOT AN INHERITANCE. The comp draws these
   square. The owner asked for the original 12px radius back, so the radius is
   ours and the rest of the treatment — the gold hairline, the scanlines, the
   0A0A0A ground — is the comp's. Do not "correct" it to match the comp. */
.demo{
  /* ⚠️ THE ACCENT IS SAGE BY DEFAULT AND GOLD ONLY ON THE PAID ONE, ruled
     7 Sep 2026. The comp draws every panel in gold. But CLAUDE.md keeps two
     identities apart on purpose: sage #7a9e7e is the APP — the extensions and
     the desktop shell — and gold is the PRINT/premium accent that the dark
     planner band and the flagship own. Three free browser tools wearing the
     flagship's gold blurred exactly the line the brand keeps.
     One custom property, set here and overridden once below, so nothing else
     in this block has to know which panel it is painting. */
  --dm-accent:#7a9e7e;
  --dm-accent-line:rgba(122,158,126,.42);
  --dm-accent-soft:rgba(122,158,126,.16);

  background:#0A0A0A;
  border:1px solid var(--dm-accent-line);
  border-radius:12px;
  position:relative;overflow:hidden;
  color:#fff;
}
/* The paid one keeps gold — it is the only panel on the page that is not an
   extension, and the only one selling something. */
.demo-vault{
  --dm-accent:var(--gold);
  --dm-accent-line:rgba(201,168,76,.38);
  --dm-accent-soft:rgba(201,168,76,.16);
}
/* The scanline wash, same device as the planner band, at the same opacity.
   Under the content on its own layer so no glyph is ever crossed by a line. */
.demo::after{
  content:"";position:absolute;inset:0;pointer-events:none;z-index:0;
  background:repeating-linear-gradient(180deg,rgba(255,255,255,.028) 0 1px,transparent 1px 4px);
}
.demo > *{position:relative;z-index:1}
@media(forced-colors:active){.demo::after{display:none}}

.dm-head{
  display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  padding:13px 15px 11px;border-bottom:1px solid #23231F;
}
.dm-name{font:400 12px/1 var(--mono);letter-spacing:.04em;color:#fff}
.dm-state{
  font:400 10px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;
  color:#A9A9A4;display:flex;align-items:center;gap:6px;
}
/* The live pip. Same 2.6s breath as the brand dot, deliberately — it is the
   same signal (this thing is alive) in the same language. */
.dm-pip{width:5px;height:5px;background:var(--dm-accent);display:block;border-radius:1px}
@media(prefers-reduced-motion:no-preference){
  .dm-pip{animation:pulse 2.6s ease-in-out infinite}
}

.dm-tabs{display:flex;border-bottom:1px solid #23231F}
.dm-tab{
  flex:1;background:transparent;border:0;border-bottom:1px solid transparent;
  color:#A9A9A4;font:400 10px/1 var(--mono);letter-spacing:.14em;
  text-transform:uppercase;padding:9px 4px 8px;cursor:pointer;
}
.dm-tab[aria-selected="true"]{border-bottom-color:var(--dm-accent);color:#fff}
.dm-tab:hover{color:#fff}

.dm-find{
  padding:9px 15px;border-bottom:1px solid #23231F;
  display:flex;align-items:center;gap:8px;
}
.dm-slash{font:400 11px/1 var(--mono);color:#7E7E7A}
.dm-input{
  flex:1;min-width:0;background:transparent;border:0;outline:none;color:#fff;
  font:400 11px/1.4 var(--mono);letter-spacing:.02em;padding:2px 0;
}
.dm-input::placeholder{color:#7E7E7A}
.dm-count{font:400 10px/1 var(--mono);color:#A9A9A4;white-space:nowrap}

.dm-list{display:flex;flex-direction:column;overflow:auto}
.dm-note{
  text-align:left;display:block;width:100%;background:transparent;
  border:0;border-bottom:1px solid #1C1C19;border-left:2px solid transparent;
  padding:11px 15px 12px;cursor:pointer;font-family:inherit;
}
.dm-note[aria-current="true"]{background:#141414;border-left-color:var(--dm-accent)}
.dm-note:hover{background:#141414}
.dm-note-t{display:block;font:400 13px/1.45 var(--sans);color:#fff}
.dm-note-m{
  display:flex;align-items:center;gap:8px;margin-top:6px;
  font:400 10px/1 var(--mono);letter-spacing:.06em;color:#A9A9A4;
}
.dm-empty{margin:0;padding:16px 15px;font:400 11px/1.7 var(--mono);color:#A9A9A4}
/* ⚠️ WITHOUT THIS THE FILTER LOOKS BROKEN AND THE TESTS STILL PASS.
   .dm-note sets display:block, which beats the user-agent's
   [hidden]{display:none} on specificity — so demos.js could set
   n.hidden = true, every assertion reading the PROPERTY would agree the
   note was hidden, and the note would sit there in plain view. Any rule
   that sets `display` on an element the script hides needs its own
   [hidden] escape, and this is that escape. */
.dm-note[hidden],.dm-empty[hidden]{display:none}

.dm-foot{
  border-top:1px solid #23231F;padding:11px 15px 13px;background:#0D0D0D;
  display:flex;flex-direction:column;gap:9px;
}
.dm-draft{
  width:100%;box-sizing:border-box;resize:none;background:#141414;
  border:1px solid #23231F;color:#fff;font:400 12.5px/1.5 var(--sans);
  padding:8px 9px;outline:none;border-radius:4px;
}
.dm-act{display:flex;align-items:center;justify-content:space-between;gap:10px}
.dm-hint{font:400 10px/1 var(--mono);letter-spacing:.06em;color:#A9A9A4}
.dm-btn{
  font:400 10px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;
  background:transparent;color:#fff;border:1px solid var(--dm-accent-line);
  padding:6px 11px;cursor:pointer;border-radius:3px;
}
.dm-btn:hover{background:var(--dm-accent-soft)}

/* vault•sorter's verdict table */
.dm-rows{display:grid;grid-template-columns:1fr auto;gap:0 16px;padding:0 16px}
.dm-cell{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid #1C1C19}
.dm-isin{font:400 11.5px/1 var(--mono);letter-spacing:.03em;color:#fff}
.dm-tick{font:400 11.5px/1 var(--mono);color:#A9A9A4}
.dm-verdict{
  display:flex;align-items:center;padding:9px 0;border-bottom:1px solid #1C1C19;
  font:400 10px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;
}
.dm-v-fund{color:var(--pp-accent,#7a9e7e)}
.dm-v-share{color:#A9A9A4}
.dm-v-unknown{color:var(--gold)}
.dm-v-reading{color:#4E4E4A}
.dm-total{
  display:flex;align-items:baseline;justify-content:space-between;gap:12px;
}
.dm-total b{font:400 26px/1 var(--display);letter-spacing:-.01em;
            font-variant-numeric:tabular-nums;color:#fff}
.dm-note-sm{margin:0;font:400 10.5px/1.65 var(--mono);letter-spacing:.02em;color:#A9A9A4}

/* site•stay's bars and tab•tally's count */
.dm-bars{list-style:none;margin:0;padding:14px 16px;display:flex;flex-direction:column;gap:10px}
.dm-bars li{display:grid;grid-template-columns:52px 1fr auto;gap:10px;align-items:center}
.dm-bar-l{font:400 10.5px/1 var(--mono);color:#A9A9A4}
.dm-bar{height:7px;background:#1C1C19;border-radius:2px;overflow:hidden}
.dm-bar i{display:block;height:100%;background:var(--pp-accent,#7a9e7e);border-radius:2px;
          transition:width .9s cubic-bezier(.2,.7,.25,1)}
.dm-bar-v{font:400 10.5px/1 var(--mono);color:#A9A9A4;font-variant-numeric:tabular-nums}
.dm-big{
  margin:0;padding:26px 16px 6px;text-align:center;
  font:400 46px/1 var(--display);letter-spacing:-.02em;color:#fff;
  font-variant-numeric:tabular-nums;
}
.dm-said{margin:0;padding:0 16px 20px;text-align:center;
         font:400 11px/1.6 var(--mono);color:var(--dm-accent);
         transition:opacity .3s ease}
.dm-said[hidden]{display:block!important;opacity:0}
@media(prefers-reduced-motion:reduce){
  .dm-bar i,.dm-said{transition:none}
}

/* ══ THE COMP'S LAYOUT ═════════════════════════════════════════════════════
   From the Claude Design comp, 7 September 2026. Two rules carry the page.

   .col2 is the whole alignment system: a label column and a content column,
   the same two on every band, so the eyebrow is the ONLY thing left of the
   content edge and the content has exactly one left edge down the page. */
.col2{display:grid;grid-template-columns:1fr;gap:14px}
@media(min-width:52rem){
  .col2{grid-template-columns:var(--col-label) minmax(0,1fr);gap:0 var(--col-gap);align-items:start}
}

/* The hero: prose, then the working panel. 320px is the comp's, and it is a
   sidebar width rather than a picture width — the panel is a likeness of a
   browser sidebar and reads wrong at any other proportion. */
.hero-grid{display:grid;grid-template-columns:1fr;gap:36px}
@media(min-width:62rem){
  .hero-grid{grid-template-columns:minmax(0,1fr) 320px;gap:0 56px;align-items:start}
}
.claims{
  margin:0;padding:14px 0 0;list-style:none;display:flex;flex-wrap:wrap;
  border-top:1px solid var(--rule-light);
  font:400 11px/1.7 var(--mono);letter-spacing:.03em;color:var(--ink-mid);
}
.claims li + li::before{content:"|";color:var(--rule-light);margin:0 10px}
.section-body{margin:0 0 34px;color:var(--ink-mid)}

/* The flagship block: argument left, the tool running right. */
.lead-product{display:grid;grid-template-columns:1fr;gap:30px;padding-bottom:46px;
              border-bottom:1px solid var(--rule-light)}
@media(min-width:62rem){
  .lead-product{grid-template-columns:var(--col-body) minmax(0,1fr);gap:0 40px;align-items:start}
}
.lead-figure{margin:0}
.lead-figure figcaption,.hero-figure figcaption{
  margin-top:10px;font:400 10px/1.6 var(--mono);letter-spacing:.06em;color:var(--ink-muted);
}
.tag-paid{
  font:400 10px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  background:var(--ink);color:var(--paper);padding:3px 7px;vertical-align:middle;
}
.receipt-title{
  margin:0 0 12px;font:400 10px/1 var(--mono);letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink-muted);
}
.cta{margin:22px 0 0}

.tier-block{padding:42px 0 0}
.tier-block + .tier-block{border-top:1px solid var(--rule-light);margin-top:46px}
.tier span{
  font:400 11px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;
  color:var(--ink-muted);white-space:nowrap;
}

/* Three tools, one row, each a column so the buttons line up at the bottom
   however uneven the prose above them is. */
.tools{display:grid;grid-template-columns:1fr;gap:34px}
@media(min-width:52rem){.tools{grid-template-columns:repeat(3,minmax(0,1fr));gap:0 var(--col-gap)}}
.tool{display:flex;flex-direction:column;gap:12px;padding-top:16px;border-top:1px solid var(--ink)}
.tool .measure{max-width:none;font-size:15px;color:var(--ink-mid)}
.tool .cta{margin-top:auto}
.tool .demo{margin:0}

/* The disclosure. Native, so it needs no script and is correct for a keyboard
   and a screen reader without being told. */
.receipt-fold summary{
  cursor:pointer;list-style:none;
  font:400 10.5px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;
  color:var(--sage);border-bottom:1px solid rgba(84,115,87,.4);
  display:inline-block;padding-bottom:2px;
}
.receipt-fold summary::-webkit-details-marker{display:none}
.receipt-fold summary:hover{color:var(--ink);border-bottom-color:var(--ink)}
.receipt-fold[open] summary{margin-bottom:14px}
.receipt-fold .receipt{margin:0;max-width:none}

.signup-grid{display:grid;grid-template-columns:1fr;gap:26px}
@media(min-width:62rem){.signup-grid{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:0 44px}}
.field-label{
  display:block;margin-bottom:12px;
  font:400 10px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--ink-muted);
}
.field-row{display:flex;gap:10px;flex-wrap:wrap}

/* ⚠️ THESE EXIST BECAUSE INLINE style="" IS BLOCKED, AND THAT IS NOT A STYLE
   PREFERENCE. netlify.toml serves style-src 'self' with no 'unsafe-inline',
   so a style attribute in the markup is DROPPED by the browser — silently, in
   production, while looking perfect under any local server that sends no CSP.
   The README records two attributes already shipped that way. Ten more were
   caught here on 7 Sep 2026, carried over from the design comp, which draws
   with inline styles because a design tool has no CSP.
   Rule: no style attribute in any template, ever. Put it here.
   (Script-set styles — el.style.width in demos.js — are CSSOM, not markup,
   and are NOT blocked. That distinction is why the bars can animate.) */
.demo-tall{display:flex;flex-direction:column;min-height:400px}
/* ⚠️ A FIXED HEIGHT, NOT A MINIMUM, AND min-height:0 ON THE LIST INSIDE IT.
   Both are needed and neither is obvious.

   The hero panel pins real notes, so its list grows without limit. With
   `min-height:460px` the panel simply got taller with every note and pushed
   the Software section down the page. A definite height is what gives the
   list something to scroll inside.

   min-height:0 is the other half. A flex item's min-height computes to `auto`,
   which is its CONTENT size — so `flex:1` plus `overflow:auto` still grows
   rather than scrolls, because the item refuses to shrink below its content.
   This is the single most common flexbox-overflow bug and it is invisible
   until something inside actually overflows. demos.js also caps the list at
   five notes, so this is the floor rather than the only defence. */
.demo-pagepin.demo-tall{height:460px}
.demo-tall .dm-list{flex:1;min-height:0}
.demo-vault .dm-rows{flex:1;align-content:start}
.dm-btn-start{align-self:flex-start}

/* The finished state, in CSS rather than in a style attribute. With JavaScript
   off these are the widths a visitor sees; demos.js winds them to 0 and grows
   them back, so the still is correct and the animation is the enhancement. */
.dm-bar i[data-bar="82"]{width:82%}
.dm-bar i[data-bar="54"]{width:54%}
.dm-bar i[data-bar="31"]{width:31%}
.dm-bar i[data-bar="18"]{width:18%}

/* ⚠️ THE THREE TOOL PANELS ARE ONE HEIGHT, and it is page•pin's — 254px at
   1280, the tallest of the three because it lists three notes. Measured, not
   picked: site•stay came out 192 and tab•tally 191, so the row read as three
   different components rather than one set of three tools.
   The panels then fill rather than top-align, so the caption sits on the floor
   of each box instead of floating halfway up an empty one. */
.tool .demo{min-height:254px;display:flex;flex-direction:column}
.tool .dm-list{flex:1}
.tool .demo .demo-hint{margin-top:auto}
.tool .demo .dm-bars{flex:1;justify-content:center}
.tool .demo .dm-big{margin-top:auto}
.tool .demo .dm-said{margin-bottom:auto}
