  /* ══════════════════════════════════════════════════════════════════
     bun & bolt — the index.

     A room, not a list. Each piece is a cube cut out of its own world and
     set down on the paper: the sand table is a block of sand whose top is
     the live tray, the Game of Life is a block of time whose top is now and
     whose sides are its history, the World Cup page is a small block of
     turf, and the two things still being made are empty glass. You can turn
     them, and — if you hold still — lift one onto another.

     Every word is real HTML in all fourteen languages (the sand table's); the 3-D scene only places
     the labels. The scene is home/src/cubes/scene.js bundled into
     web/cubes.js (home/tools/build_cubes.mjs) and loaded by the one page
     script below, which appends it as a module <script> — so the body carries
     no <script src> of its own, and a
     reader without WebGL, or with reduced motion, gets a still picture of
     the room and the same links as a plain list.
     ══════════════════════════════════════════════════════════════════ */
  /* ── type: a hand, not a grotesque (2026-10-07) ─────────────────────────
     The site is a room of toys, and the owners found the grotesque it used to
     be set in too stiff for it. Every script now gets a face that looks
     written rather than engineered — a little personality, never so much
     that a label stops being readable at .74rem:
       Latin + Cyrillic   Balsamiq Sans — a sketch hand; it is the same
                          wobble as the pen-drawn field and button below
       Chinese (both)     LXGW WenKai TC — a pencil kai drawn from Klee One;
                          it carries the Simplified characters too
       Japanese           Klee One — the textbook pencil hand
       Korean             Gaegu — handwriting, rounded
       Arabic             Baloo Bhaijaan 2 — the rounded, friendly cut
     Latin and Cyrillic are Google Fonts' whole subsets (a reader types into
     the sign-up field in them, so they cannot be cut to the page's letters);
     unicode-range keeps each from loading where it is not needed. The CJK,
     Korean and Arabic faces are cut per page by home/tools/cut_fonts.py from
     that page's own copy — re-run it after editing copy in those languages,
     and bump _vN if a file ever ships. Family names are the site's own
     ("Bun …"), so a copy installed on the reader's machine can never stand
     in for the subset with its different glyph set. */
  @font-face {
    font-family: "Balsamiq Sans"; font-style: normal; font-weight: 400; font-display: swap;
    src: url("/home/web/balsamiq_latin_400_v1.woff2?v=b56e29d52f7e") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  }
  @font-face {
    font-family: "Balsamiq Sans"; font-style: normal; font-weight: 400; font-display: swap;
    src: url("/home/web/balsamiq_latinext_400_v1.woff2?v=d7e23d7b7df4") format("woff2");
    unicode-range: U+0100-0130, U+0132-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
  }
  @font-face {
    font-family: "Balsamiq Sans"; font-style: normal; font-weight: 400; font-display: swap;
    src: url("/home/web/balsamiq_cyrillic_400_v1.woff2?v=96fb0eab544e") format("woff2");
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
  }
  @font-face {
    font-family: "Balsamiq Sans"; font-style: normal; font-weight: 700; font-display: swap;
    src: url("/home/web/balsamiq_latin_700_v1.woff2?v=b059cf0c22a2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  }
  @font-face {
    font-family: "Balsamiq Sans"; font-style: normal; font-weight: 700; font-display: swap;
    src: url("/home/web/balsamiq_latinext_700_v1.woff2?v=d892d30c0b27") format("woff2");
    unicode-range: U+0100-0130, U+0132-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
  }
  @font-face {
    font-family: "Balsamiq Sans"; font-style: normal; font-weight: 700; font-display: swap;
    src: url("/home/web/balsamiq_cyrillic_700_v1.woff2?v=7256daa0790d") format("woff2");
    unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
  }



  @font-face {
    font-family: "Bun Klee"; font-style: normal; font-weight: 400; font-display: swap;
    src: url("/home/web/klee_ja_400_v1.woff2?v=76d9871d255c") format("woff2");
    unicode-range: U+1100-11FF, U+2E80-2EFF, U+3000-30FF, U+3130-318F, U+3200-4DBF, U+4E00-9FFF, U+AC00-D7AF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF;
  }
  @font-face {
    font-family: "Bun Klee"; font-style: normal; font-weight: 600; font-display: swap;
    src: url("/home/web/klee_ja_600_v1.woff2?v=11f317daa41b") format("woff2");
    unicode-range: U+1100-11FF, U+2E80-2EFF, U+3000-30FF, U+3130-318F, U+3200-4DBF, U+4E00-9FFF, U+AC00-D7AF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF;
  }




  :root {
    color-scheme: light;

    --ink:    #12271E;   /* 15.4:1 on white */
    --ink-2:  #3D5148;   /* body prose */
    --ink-3:  #5C6F66;   /* labels, dates, the quiet register */
    --paper:  #F1EEE8;   /* the room's paper: scene.js clears to the same colour */
    --rule:   #E3E8E5;   /* hairlines between entries */
    --pitch:  #2C6320;   /* the house green; here it is only ever a verb */
    /* The wordmark's own green, as drawn. It is NOT --ink: they are two
       different dark greens four steps apart, which is close enough that a
       reader will never see it as a decision and just slightly off. Kept as
       delivered rather than quietly snapped to --ink, because the mark is
       the one thing on this page whose colour is not this stylesheet's call.
       One place to change it if it should match. */
    --brand:  #12392B;

    /* One stack for every language. Balsamiq is first and its ranges are
       Latin and Cyrillic only, so every other script falls straight through
       to --script, the page's own hand for it (below). */
    --font-body: "Balsamiq Sans", var(--script), -apple-system, BlinkMacSystemFont, "Segoe UI",
                 Roboto, "Helvetica Neue", Arial, sans-serif;
    /* The small numbered labels (01, EARLIER) used to be monospace, which read
       as a spec sheet; they are in the hand now too. The name is kept so the
       rules that use it need not change. */
    --font-mono: var(--font-body);

    /* Wider than a reading column, because the content is now pictures. At
       660 — the width this page had when every entry was a paragraph — two
       staggered thumbnails come out barely 300px each, and a shop window
       whose goods are that small is a list again with extra steps. */
    --col: 940px;
    --gut: 22px;
  }

  /* --script: the face for whatever Balsamiq cannot draw, chosen by the
     language of the element rather than of the page. Keyed to :lang() so the
     language menu's own links, each marked with its language, pick theirs too:
     日本語 in a Japanese face and 繁體中文 in a Traditional one on every page.
     Each page's own subset ("Bun WenKai SC/TC", "Bun Klee", "Bun Gaegu",
     "Bun Baloo") leads its list; on the other pages that name resolves to
     nothing and the system face follows — whole, never half a word in one
     face and half in another, which is why the two Chinese pages name their
     subsets differently. */
  /* The default is the system's own UI face: Cyrillic, and anything else no
     rule below names, wants that — not a Chinese face that happens to carry
     Cyrillic too (the first draft set the ru page in PingFang SC). */
  :root { --script: system-ui; }
  /* …and it has to be said again on every element marked with such a language,
     because a custom property inherits: without this, Русский in the menu of
     the ja page took the ja stack and came out in Hiragino's full-width
     Cyrillic, one letter per em. */
  [lang]:not(:lang(zh)):not(:lang(ja)):not(:lang(ko)):not(:lang(ar)) { --script: system-ui; }
  :lang(zh-Hans) { --script: "Bun WenKai SC", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC"; }
  :lang(zh-Hant) { --script: "Bun WenKai TC", "PingFang TC", "Microsoft JhengHei", "Noto Sans CJK TC"; }
  :lang(ja)      { --script: "Bun Klee", "Hiragino Sans", "Hiragino Kaku Gothic ProN", "Yu Gothic", "Meiryo", "Noto Sans CJK JP"; }
  :lang(ko)      { --script: "Bun Gaegu", "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans CJK KR"; }
  :lang(ar)      { --script: "Bun Baloo", "Geeza Pro", "Segoe UI", "Noto Sans Arabic", Tahoma; }

  * { box-sizing: border-box; }

  /* clip, NOT hidden. Bare `overflow-x: hidden` computes overflow-y to
     `auto` and quietly turns html and body into scroll containers — the
     bug that made the football page refuse to scroll for a week. Nothing
     on this page pokes out of its box today, but the rule is the site's,
     not the page's. `hidden` stays as the pre-2022-engine fallback. */
  html { overflow-x: hidden; overflow-x: clip; }

  body {
    margin: 0;
    background: var(--paper);
    color: var(--ink);
    font-family: var(--font-body);
    font-size: 17px;
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
  }

  /* 汉字 want more leading than Latin at the same size. The family is NOT
     switched here — see the stack above. text-autospace is the engine-level
     fix for the CJK/Latin gap, which is why no space is typed by hand
     anywhere in the zh copy; engines without it yet lose nothing. */
  :root:is([data-lang="zh"], [data-lang="zh-hant"], [data-lang="ja"], [data-lang="ko"]) body { line-height: 1.75; text-autospace: normal; }
  /* Arabic letters join, and letter-spacing pulls the joins apart — every
     tracked label on this page has to drop it there, not just look wider. */
  :root[data-lang="ar"] body { line-height: 1.75; }
  :root[data-lang="ar"] * { letter-spacing: 0 !important; }
  /* Gaegu draws small inside its em and thin at label sizes: it is scaled up in
     its @font-face (size-adjust), and Hangul does not want the wide tracking the
     Chinese and Japanese labels carry — spaced out, a hand-written word falls
     apart into separate syllables. */
  :root[data-lang="ko"] .room .t-no { letter-spacing: .02em; }

  .wrap { max-width: var(--col); margin: 0 auto; padding: 0 var(--gut); }

  /* ── streaming curtain ─────────────────────────────────────────────
     The head (site-level) locks scroll and holds this cover until the DOM
     is complete. Its colours must match meta.json's splash_bg/splash_fg,
     which paint before this stylesheet exists. */
  #splash { gap: 10px; }
  #splash .sp-mark { display: block; width: 150px; height: auto; color: var(--brand); }
  #splash .sp-txt { color: var(--ink-3); font-weight: 500; }

  /* ── masthead: removed ─────────────────────────────────────────────
     There used to be a wordmark and a hairline here. Both are gone at the
     owners' word: the drawing opens the page now, and the bun / bolt marks
     drawn inside it are the site saying its own name. The h1 keeps the
     string for machines; the drawn wordmark still signs the footer and
     the curtain, which is why .mark survives below. */
  .mark { margin: 0; line-height: 0; }
  .mark .wordmark {
    display: block; width: clamp(160px, 24vw, 200px); height: auto;
    color: var(--brand);
  }
  /* Off-screen but not display:none — the text has to stay in the accessibility
     tree and in the document a crawler reads. clip-path over the old clip rect,
     and nowrap so a long string can't be reflowed into a 1px column and read
     one letter per line. */
  .vh {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }

  /* ── language ──────────────────────────────────────────────────────
     One mark that opens, rather than four codes standing there all the
     time. This page has one job — get you into one of five pieces — and
     four permanent chips at the top were four more things competing with
     those five, over a decision most readers make once, or never.

     <details>, so it still needs no script. A disclosure is the one piece
     of interaction HTML has been able to do by itself for years, and the
     page's rule is that it carries nothing it can do without. The cost is
     that it will not close when you click elsewhere — that part does want
     JavaScript — and clicking the globe a second time is not a hardship.

     The mark is drawn, not typed. The obvious alternative is 文A, the way
     Wikipedia writes it, and it is wrong HERE: no CJK subset is loaded on
     the en/es/pt pages, so that 文 would be set in whatever face the
     reader's system happens to offer. The menu below accepts exactly that
     trade for 中文, because a reader looking for their own language needs
     to see their own language; a piece of the site's own furniture does
     not get the same excuse, so it is drawn in the site's own line. */
  /* Anchored to the column's top-right corner, over the drawing's empty
     sky — the masthead row it used to sit in no longer exists. Absolute
     against .intro, whose width IS the column; z-index because on narrow
     screens it floats above the scene itself. */
  #lang { position: absolute; z-index: 30; top: clamp(10px, 2vw, 18px); right: 0; }

  .lang-btn {
    display: block; padding: 7px; margin-right: -7px;
    cursor: pointer; color: var(--ink-3);
    border-radius: 5px;
    list-style: none;                              /* the disclosure triangle… */
  }
  .lang-btn::-webkit-details-marker { display: none; }   /* …and again, for older WebKit */
  .lang-btn svg { display: block; width: 20px; height: 20px; }
  .lang-btn:hover, #lang[open] > .lang-btn { color: var(--ink); background: #F2F5F3; }
  .lang-btn:focus-visible { outline: 2px solid var(--pitch); outline-offset: 2px; }

  /* Absolute, so opening it moves nothing: the masthead is a fixed line and
     a menu that pushed the page down would move the first thumbnail out from
     under whoever is reading it. z-index because the drawing below opens a
     painting layer of its own further down the document. */
  /* Two columns since the menu went from four languages to fourteen: one
     column of fourteen is taller than a phone's first screen of the room. */
  .lang-menu {
    position: absolute; z-index: 20;
    top: calc(100% + 8px); right: 0;
    display: grid; grid-template-columns: auto auto; gap: 2px; padding: 6px;
    background: var(--paper); border: 2px solid var(--ink);
  }
  /* Say the quiet part. A closed <details> already hides this — but it does it
     by containment on a pseudo-element the browser owns, and an absolutely
     positioned child is exactly the case where engines have historically
     disagreed about whether the containment still reaches it. Measured in
     Chrome it is hidden and unfocusable while still reporting a full 96×148
     box, which is the shape of a thing being hidden by a mechanism I cannot
     see. One line makes it a rule this stylesheet states rather than one it
     inherits. */
  #lang:not([open]) .lang-menu { display: none; }
  .lang-menu a {
    font-family: "Balsamiq Sans", var(--script), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    font-size: .82rem; font-weight: 600;
    color: var(--ink-3); text-decoration: none; white-space: nowrap;
    padding: 5px 10px; border-radius: 4px;
  }
  .lang-menu a:hover { color: var(--ink); background: #F2F5F3; }
  .lang-menu a[aria-current="page"] { color: var(--paper); background: var(--ink); }
  /* 汉字 carry far more strokes per em than Latin and turn to mud at the same
     size, so they need a floor: bigger.
     The floor follows the SCRIPT OF THE LABEL, not the language of the page.
     This menu lists 中文 on every page, so it needs the floor on every page;
     keying it to :root[data-lang="zh"] left the row unreadable everywhere
     else, which is where a reader who cannot read this page is most likely to
     be looking for it. */
  .lang-menu a:is([data-set="zh"], [data-set="zh-hant"], [data-set="ja"], [data-set="ko"], [data-set="ar"]) { font-size: .88rem; }
  .lang-menu a[dir="rtl"] { text-align: right; }

  /* The sprite is 0×0 but inline, so without this it still sits in a line box
     and pushes the room down by one line height. */
  .sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

  /* ── the room ──────────────────────────────────────────────────────
     Full bleed, one viewport tall on a screen wider than it is tall, and a
     fixed 1800px column on a phone: scene.js lays the cubes out in a row or
     a column from the canvas's own aspect, so this rule and that layout are
     one contract — change them together. The paper inside the canvas and
     the paper of the page are the same colour (--paper), so the room has no
     edges. */
  .room { position: relative; height: 100vh; height: 100svh; min-height: 620px; overflow: hidden; }
  @media (max-aspect-ratio: 9/10) { .room { height: 1800px; } }
  .room canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block;
    touch-action: pan-y; user-select: none; -webkit-user-select: none; opacity: 0; transition: opacity .6s; }
  .room.live canvas { opacity: 1; }
  /* The still: a picture of the same room. Behind the canvas while the scene
     loads, and the whole room for a reader without WebGL or with reduced
     motion — then the labels drop out of the 3-D layout into a plain list. */
  .room picture { display: contents; }
  .room-still { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transition: opacity .6s; }
  .room.live .room-still { opacity: 0; }
  .room-top { position: absolute; z-index: 2; top: 0; left: 0; right: 0; padding-top: clamp(28px, 5.5vh, 56px);
    text-align: center; pointer-events: none; }
  .room-top .wordmark { display: inline-block; width: min(240px, 58vw); height: auto; color: var(--ink); }
  .room #lang { top: clamp(14px, 2.4vw, 22px); right: clamp(14px, 2.4vw, 26px); }

  .tag { position: absolute; z-index: 2; left: 0; top: 0; white-space: nowrap; text-align: center;
    color: var(--ink); text-decoration: none; opacity: 0; will-change: transform; transition: opacity .3s; }
  .room.live .tag { opacity: 1; }
  /* The links work before the scene arrives, even if its request never finishes. */
  .room:not(.live):not(.still) .tags {
    position: absolute; z-index: 2; inset-inline: 0; bottom: 16px;
    display: flex; flex-wrap: wrap; justify-content: center; gap: 16px 28px;
    padding: 16px var(--gut); background: var(--paper);
  }
  .room:not(.live) .tag { position: static; opacity: 1 !important; transform: none !important; pointer-events: auto !important; }

  .t-no { display: block; font: 500 .62rem/1 var(--font-mono); letter-spacing: .24em; color: var(--ink-3); }
  .t-name { display: block; margin-top: 7px; font-size: clamp(1.05rem, 1.55vw, 1.35rem); font-weight: 600; letter-spacing: -.012em; line-height: 1.1; }
  :root:is([data-lang="zh"], [data-lang="zh-hant"], [data-lang="ja"], [data-lang="ko"], [data-lang="ar"]) .t-no { font-size: .74rem; letter-spacing: .1em; }
  .t-go { display: inline-block; margin-inline-start: 6px; font-size: .9rem; transition: transform .35s cubic-bezier(.2,0,0,1); }
  a.tag:hover .t-go { transform: translateX(4px); }
  /* Right to left the door is on the other side: the arrow turns round, and
     so does the nudge it gives on hover. */
  [dir="rtl"] .t-go { transform: scaleX(-1); }
  [dir="rtl"] a.tag:hover .t-go { transform: scaleX(-1) translateX(4px); }
  a.tag:focus-visible { outline: 2px solid var(--pitch); outline-offset: 4px; }
  .tag.soon .t-name { color: var(--ink-3); font-weight: 500; }
  .tag.small .t-name { font-size: clamp(.9rem, 1.1vw, 1.02rem); }
  /* On a phone the room is one column and the small cube sits near the left
     edge, so a long name centred under it ran off the screen (French and
     Russian by ~45px). There, a name may wrap — balanced, so two even lines
     rather than one long and one stray word. */
  @media (max-aspect-ratio: 9/10) {
    .tag .t-name { white-space: normal; max-width: 150px; text-wrap: balance; }
    /* Japanese has no spaces to break at, and the engine's own guess split
       記|念; it may break only where the copy says so (<wbr>). */
    :root[data-lang="ja"] .tag .t-name { word-break: keep-all; }
  }


  /* No WebGL, reduced motion, or no script at all: the picture, then the doors as a list.
     (The <noscript> after the room repeats these rules for the no-script case.) */
  .room.still { height: auto; min-height: 0; }
  .room.still canvas { display: none; }
  .room.still .room-still { position: static; display: block; height: auto; }
  .room.still .tags { display: grid; gap: 18px 28px; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    max-width: var(--col); margin: 0 auto; padding: 28px var(--gut) 8px; }
  .room.still .tag { position: static; opacity: 1; transform: none !important; text-align: start; white-space: normal; }

  /* ── keeping in touch ──────────────────────────────────────────────
     A place to leave an address, last on the page, because it is the
     question that only makes sense after you have seen the five things and
     not before. One field and one button: a name box would be a second
     thing to fill in for a mail that opens "here is a new one".

     No script. The form posts, the mail service answers with its own
     confirmation page, and that is the whole flow — the same reasoning as
     the language menu. An inline "thanks!" without leaving the page is the
     one thing here that would genuinely need JavaScript, and it is worth
     asking for rather than assuming. */
  /* One centred column: the line, then the field right under it. Side by
     side across a 940px column the two ended up at opposite edges and read as
     unrelated; stacked and close, the sentence is the field's own label. */
  .subscribe {
    position: relative; z-index: 3;   /* above the room's canvas, which it now tucks into */
    margin-top: clamp(4px, 1.5vw, 16px);
    display: grid; gap: 12px; justify-items: center; text-align: center;
  }
  /* Pulled up into the room's empty foot, but only while the room is live:
     there its labels end at least ~63px above its bottom edge (measured, worst
     case 1280×720). In the still room the labels become a list that ends at
     that edge, and this would sit on top of it. */
  .room.live ~ .wrap .subscribe { margin-top: -36px; }
  .subscribe-say { margin: 0; max-width: 42ch; font-size: 1.02rem; color: var(--ink-2); line-height: 1.6; }
  .subscribe-say b { color: var(--ink); font-weight: 700; }
  :root:is([data-lang="zh"], [data-lang="zh-hant"], [data-lang="ja"], [data-lang="ko"], [data-lang="ar"]) .subscribe-say { font-size: .95rem; }

  .subscribe-form { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; justify-content: center; }
  /* The field and the button are drawn with the wordmark's pen: one thin
     line in its green, a little unsteady, the corners crossing where one
     stroke ends and the next begins. Each outline is an inline svg stretched
     over its control; vector-effect keeps the line one pen wide whatever the
     stretch, so a phone and a desktop draw the same line. The field's svg sits
     ON TOP of the input (pointer-events off) so an autofill tint, which fills
     the input's rectangle, can never paint over the drawing. */
  .pen { position: relative; display: inline-block; }
  .pen > svg, .subscribe-form button > svg {
    position: absolute; inset: 0; width: 100%; height: 100%;
    overflow: visible; pointer-events: none;
  }
  .pen > svg path, .subscribe-form button > svg path {
    fill: none; stroke: var(--brand); stroke-width: 1.6;
    stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke;
  }
  .pen > svg { z-index: 1; }
  .subscribe-form input {
    display: block; font: inherit; font-size: .92rem;
    padding: 11px 14px; min-width: 0; width: clamp(200px, 50vw, 260px);
    color: var(--ink); background: transparent;
    border: 0; border-radius: 0; outline: 0;
    -webkit-appearance: none; appearance: none;
  }
  .subscribe-form input:-webkit-autofill { -webkit-box-shadow: 0 0 0 40px var(--paper) inset; -webkit-text-fill-color: var(--ink); }
  /* An address is Latin and reads left to right on every page, Arabic too. */
  [dir="rtl"] .subscribe-form input { direction: ltr; text-align: right; }
  .subscribe-form input::placeholder { color: var(--ink-3); opacity: 1; }
  /* Focus is the same pen pressing harder, in the house green: a square
     outline around a drawn box would read as a second, machine box. */
  .pen:focus-within > svg path { stroke: var(--pitch); stroke-width: 2.6; }
  .subscribe-form button {
    position: relative; isolation: isolate;
    font: inherit; font-size: .88rem; font-weight: 700; letter-spacing: .01em;
    padding: 12px 20px; cursor: pointer; white-space: nowrap;
    color: var(--paper); background: none; border: 0; border-radius: 0;
  }
  .subscribe-form button > svg { z-index: -1; }
  .subscribe-form button > svg path { fill: var(--brand); }
  .subscribe-form button:hover > svg path { fill: var(--pitch); stroke: var(--pitch); }
  .subscribe-form button:focus-visible { outline: 2px solid var(--pitch); outline-offset: 4px; }
  .subscribe-form button:disabled { cursor: progress; }
  /* The promise, kept small and kept honest. It sits under the field rather
     than under the sentence because it is a condition of the thing you are
     about to do, not part of the pitch. */
  /* width:0 + min-width:100%: the note wraps to the field and button above it
     instead of widening the column and squeezing the sentence beside it. */
  .subscribe-note { flex-basis: 100%; width: 0; min-width: 100%; margin: 0; font-size: .78rem; color: var(--ink-3); line-height: 1.5; }
  .subscribe-check:empty { display: none; }
  .subscribe-check { flex-basis: 100%; }
  .subscribe-status { flex-basis: 100%; margin: 0; font-size: .88rem; color: var(--ink); line-height: 1.5; }
  .subscribe-status:not(:has(> span:not([hidden]))) { display: none; }
  .subscribe-form.sent .pen, .subscribe-form.sent button, .subscribe-form.sent .subscribe-note { display: none; }
  :root:is([data-lang="zh"], [data-lang="zh-hant"], [data-lang="ja"], [data-lang="ko"], [data-lang="ar"]) .subscribe-note { font-size: .82rem; }

  /* Where else to find us, under the address field: the other way of being
     "called over". Handles, not words, so the row reads the same on every
     page; the network's name is in each link for a screen reader. Handles
     are Latin and stay left to right on the Arabic page too. */
  .social { margin: 4px 0 0; display: flex; flex-wrap: wrap; justify-content: center; gap: 6px 22px; font-size: .88rem; direction: ltr; }
  .social a { display: inline-flex; align-items: center; gap: 7px; color: var(--ink-2); text-decoration: none; }
  .social a:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
  .social a:focus-visible { outline: 2px solid var(--pitch); outline-offset: 3px; }
  .social svg { width: 16px; height: 16px; flex: none; }

  /* ── colophon ──────────────────────────────────────────────────────
     Nothing renders in this register today: the two blocks that described
     the site to its reader have been removed for good, and what is left is
     one address block still held back by the omit markers. The rules stay
     because that block ships by deleting those markers, and a style deleted
     alongside the last thing using it is a style someone has to reinvent
     from a screenshot. auto-fit rather than a fixed column count, for the
     same reason: whatever ends up here should lay itself out without a
     breakpoint being invented for the occasion. */
  .colophon {
    margin-top: clamp(34px, 6vw, 56px);
    padding: clamp(30px, 5vw, 40px) 0 clamp(50px, 9vw, 80px);
    border-top: 1px solid var(--rule);
    display: grid; gap: clamp(26px, 4vw, 34px);
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  }
  .colophon h2 {
    font-family: var(--font-mono);
    font-size: .7rem; font-weight: 600; letter-spacing: .17em; text-transform: uppercase;
    color: var(--ink-3); margin: 0 0 .8em;
  }
  :root:is([data-lang="zh"], [data-lang="zh-hant"], [data-lang="ja"], [data-lang="ko"], [data-lang="ar"]) .colophon h2 {
    font-family: var(--font-body); font-size: .84rem; letter-spacing: .06em; text-transform: none;
  }
  .colophon p { margin: 0; max-width: 46ch; font-size: .92rem; color: var(--ink-2); line-height: 1.66; }
  :root:is([data-lang="zh"], [data-lang="zh-hant"], [data-lang="ja"], [data-lang="ko"], [data-lang="ar"]) .colophon p { font-size: .95rem; }

  /* ── the old opening drawing, restored 2026-10-07 as the foot of the page ── */
  /* 560 is 80% of the 700 the scene opened at: now that it IS the page's
     opening, it should read as a scene, not a hero banner. */
  .intro-scene {
    position: relative; overflow: hidden;
    width: 100%; max-width: 560px; margin: 0 auto;
    aspect-ratio: 1384 / 834;
    --art-ink: #111;
    --loop: 6s;
  }
  /* The full 1672×941 canvas, positioned so that the 1384×834 window
     starting at (185, 44) — the crop — is what the overflow:hidden
     parent actually shows. Percentages here resolve against the PARENT's
     own box (width-like properties against its width, height-like ones
     against its height), which is what keeps this proportional at any
     size without a pixel of arithmetic done at runtime. */
  .intro-scene__frame {
    position: absolute;
    width: calc(100% * 1672 / 1384); height: calc(100% * 941 / 834);
    left: calc(100% * -185 / 1384); top: calc(100% * -44 / 834);
  }
  .intro-scene__base, .intro-scene__motion {
    position: absolute; inset: 0; display: block; width: 100%; height: 100%;
  }
  .intro-scene__motion { pointer-events: none; }

  .intro-scene .ink {
    fill: none; stroke: var(--art-ink); stroke-width: 3;
    stroke-linecap: round; stroke-linejoin: round;
  }
  .intro-scene .thought-mask { fill: #fff; stroke: none; }   /* white, not --paper: the frame multiplies onto the page, so white IS the page */
  /* The two name labels. Brand green, not the near-black the drawing is in,
     so they read as the site labelling its own picture rather than as two
     more things someone drew into the scene. Deliberately NOT .ink — that
     class would override the wordmark's own 9-unit stroke with the overlay's
     3 and thin the letters to wire. */
  .intro-scene .scene-mark { color: var(--brand); }

  .intro-scene .thought { opacity: 0; transform-box: fill-box; transform-origin: center; }
  .intro-scene .thought--bolt    { animation: iscene-bolt    var(--loop) ease-in-out infinite; }
  .intro-scene .thought--network { animation: iscene-network var(--loop) ease-in-out infinite; }
  .intro-scene .thought--maze    { animation: iscene-maze    var(--loop) ease-in-out infinite; }
  .intro-scene .bolt-fill { fill: var(--art-ink); }

  .intro-scene .typing-hand { transform-box: fill-box; transform-origin: center; }
  .intro-scene .typing-hand--left  { animation: iscene-tap-l .08s linear infinite alternate; }
  .intro-scene .typing-hand--right { animation: iscene-tap-r .08s linear infinite alternate; }
  .intro-scene .tap-mark { opacity: .15; animation: iscene-tap-mark .16s steps(2, end) infinite; }

  .intro-scene .product { opacity: 0; transform-box: fill-box; transform-origin: center; }
  .intro-scene .product--one   { animation: iscene-product-1 var(--loop) cubic-bezier(.2,.8,.3,1) infinite; }
  .intro-scene .product--two   { animation: iscene-product-2 var(--loop) cubic-bezier(.2,.8,.3,1) infinite; }
  .intro-scene .product--three { animation: iscene-product-3 var(--loop) cubic-bezier(.2,.8,.3,1) infinite; }
  .intro-scene .eject-mark { opacity: 0; animation: iscene-eject var(--loop) ease-out infinite; }

  @keyframes iscene-bolt {
    0%, 2% { opacity: 0; transform: translateY(7px) scale(.82); }
    6%, 25% { opacity: 1; transform: translateY(0) scale(1); }
    30%, 100% { opacity: 0; transform: translateY(-4px) scale(.94); }
  }
  @keyframes iscene-network {
    0%, 29% { opacity: 0; transform: translateY(7px) scale(.82); }
    34%, 53% { opacity: 1; transform: translateY(0) scale(1); }
    58%, 100% { opacity: 0; transform: translateY(-4px) scale(.94); }
  }
  @keyframes iscene-maze {
    0%, 57% { opacity: 0; transform: translateY(7px) scale(.82); }
    62%, 84% { opacity: 1; transform: translateY(0) scale(1); }
    91%, 100% { opacity: 0; transform: translateY(-4px) scale(.94); }
  }
  @keyframes iscene-tap-l { from { transform: translateY(-2px) rotate(-1deg); } to { transform: translateY(4px) rotate(1deg); } }
  @keyframes iscene-tap-r { from { transform: translateY(4px) rotate(1deg); } to { transform: translateY(-2px) rotate(-1deg); } }
  @keyframes iscene-tap-mark { 0%, 49% { opacity: .12; } 50%, 100% { opacity: .72; } }
  @keyframes iscene-product-1 {
    0%, 14% { opacity: 0; transform: translate(74px, -22px) rotate(7deg) scale(.65); }
    20%, 88% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); }
    94%, 100% { opacity: 0; transform: translate(0, 10px) scale(.96); }
  }
  @keyframes iscene-product-2 {
    0%, 41% { opacity: 0; transform: translate(68px, 19px) rotate(-7deg) scale(.65); }
    47%, 88% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); }
    94%, 100% { opacity: 0; transform: translate(0, 10px) scale(.96); }
  }
  @keyframes iscene-product-3 {
    0%, 68% { opacity: 0; transform: translate(63px, 58px) rotate(7deg) scale(.65); }
    74%, 88% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); }
    94%, 100% { opacity: 0; transform: translate(0, 10px) scale(.96); }
  }
  @keyframes iscene-eject {
    0%, 13%, 21%, 40%, 48%, 67%, 75%, 100% { opacity: 0; }
    15%, 18%, 42%, 45%, 69%, 72% { opacity: .85; }
  }

  /* The resting frame: the last thought the horse had, and all three pages
     it led to, held rather than looping — the same "settled" reading a
     visitor with JavaScript off would get if the base art still carried its
     own baked-in doodles (it no longer needs to: this cut erases them
     unconditionally, motion or not, and this is the one thought the CSS
     itself chooses to leave standing). */
  @media (prefers-reduced-motion: reduce) {
    .intro-scene .thought, .intro-scene .typing-hand, .intro-scene .tap-mark,
    .intro-scene .product, .intro-scene .eject-mark { animation: none; }
    .intro-scene .thought--maze, .intro-scene .product { opacity: 1; }
  }

  /* ── the two of us, at the foot of the page ─────────────────────────
     The drawing that opened the old index, kept small as a sign-off. The
     base image is white paper; the frame multiplies onto the page, so its
     white turns into whatever paper colour the room's light has given the
     page (scene.js sets --paper), and there is no white box on coloured paper. */
  .foot-scene { width: min(340px, 72vw); margin: clamp(40px, 7vw, 72px) auto 0; }
  .foot-scene .intro-scene__frame { mix-blend-mode: multiply; }

  .end {
    padding: 20px 0 40px;
    display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
    font-size: .8rem; color: var(--ink-3);
  }
  /* Signature size, and quieter than the masthead: the foot of the page is
     where the mark is a sign-off, not an entrance. align-items goes from
     baseline to center on this row because there is no longer any text on the
     left to share a baseline with — an SVG's baseline is its bottom edge, so
     baseline alignment would have hung the domain name off the mark's feet. */
  .end .mark .wordmark { width: 112px; color: var(--ink-3); }
  :root:is([data-lang="zh"], [data-lang="zh-hant"], [data-lang="ja"], [data-lang="ko"], [data-lang="ar"]) .end { font-size: .86rem; }

  @media (prefers-reduced-motion: reduce) {
    * { transition: none !important; }
  }
