/* ===================================================================
   Yama Arashi Ryu -- Classical Martial Arts, Denver
   Built from the Google Stitch template (screen-continuous.png).

   Palette and geometry are sampled from that PNG, not guessed:
     #1b1b1f  ink / dark sections      #b52426  the single red
     #f5f3ef  paper (28% of the page)  #ffffff  cards
     #2f2f32  dark card                #efeeea  rules

   No JavaScript anywhere. The stepper in "Anatomy of Kuzushi" is
   CSS-only, driven by radio inputs.
   =================================================================== */

/* -------------------------------------------------------------------
   Noto Serif (headings) and Hanken Grotesk (body).

   These are the two families the Stitch design actually specifies --
   confirmed from the project's designTheme via the Stitch MCP, not
   guessed from the screenshot. Both are SIL Open Font License, so
   self-hosting them is allowed and is what we do: no fonts.googleapis.com,
   no fonts.gstatic.com, nothing for the CSP to make an exception for.

   These are VARIABLE fonts -- one file covers weights 400-700, which is
   why there are four files rather than sixteen. Latin and Latin-Extended
   only; the Cyrillic, Greek, Vietnamese and math subsets Google also
   serves are dropped, since the site is in English.

   Regenerate with scripts/fetch-fonts.sh if the design changes families.
   ------------------------------------------------------------------- */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/hanken-grotesk-italic-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/hanken-grotesk-italic-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/hanken-grotesk-normal-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/hanken-grotesk-normal-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Noto Serif';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/noto-serif-italic-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Noto Serif';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/noto-serif-italic-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Noto Serif';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/noto-serif-normal-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Noto Serif';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/noto-serif-normal-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* -------------------------------------------------------------------
   The school font.

   Source: YamaArashiRyu.ttf (FontForge, 2024-03-13), kept in fonts-src/.
   Served as woff2 with a woff fallback. Self-hosted -- no Google Fonts,
   no CDN, nothing the CSP would have to allow.

   DRAWN BY ONE OF THE DOJO'S OWN INSTRUCTORS. These letterforms are
   somebody's handwriting, not a stock face, so the quirks below are
   characteristics and not defects. The capital H reads close to a
   lowercase h because that is how he writes it -- headings are worded to
   avoid a leading H rather than the font being touched. Don't "correct"
   the glyphs.

   TWO LIMITS, both read off the font file itself:

   1. ASCII only -- 97 mapped characters, 100 glyphs. No accented
      letters, no curly apostrophe, no en/em dash. Any of those falls
      back mid-word to a different typeface and reads as a bug. So the
      apostrophe in "KID'S KARATE" is deliberately straight, and this
      font is never used on text containing macrons (Jujitsu, Kuzushi,
      Ukemi are safe; a name like "Munoz" would not be).

   2. Blackletter display face. Unreadable as body copy, and ALL CAPS
      is worse than mixed case -- the capitals lose the ascenders that
      tell them apart. The template's headings are all-caps, so the
      school font is NOT a drop-in replacement for them.

   Where it is used, therefore: the wordmark (header and footer), the
   dojo's name inside a heading, and the pledge attribution. Mixed
   case, 24px and up, always. Class: .brand
   ------------------------------------------------------------------- */
@font-face {
  font-family: "Yama Arashi";
  src: url("/fonts/YamaArashiRyu.woff2") format("woff2"),
       url("/fonts/YamaArashiRyu.woff")  format("woff");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  --ink:        #1b1b1f;   /* dark surfaces (Stitch primary_container) */
  --text:       #1b1c1a;   /* body text   (Stitch on_surface) */
  --ink-2:      #2f2f32;
  --ink-3:      #121214;
  /* Aged paper. A deliberate departure from the Stitch-sampled values,
     which were #f5f3ef / #fbf9f5.

     The first attempt at this was #fffcdf / #fffef0, and Astone named the
     problem exactly, 2026-09-24: "I thought the yellow color from before
     would give the feeling of old paper but instead it looks like pee."

     He was right and the arithmetic says why. That pair sat at full
     brightness in red and green (255 and 252) with only blue pulled down,
     to 223. A colour that is max-luminance and short of blue IS pale
     yellow; there is nothing else it can be. Real aged paper does not work
     like that -- it pulls ALL THREE channels down and keeps the red-to-blue
     gap modest. #f7f2e7 is 247/242/231: a 16-point spread instead of 32,
     and eight points off the ceiling. That reads as warm and old rather
     than yellow.

     The texture multiplies on top, so --paper actually lands near #e8e3d9
     on screen, not #f7f2e7. Contrast rechecked against that: body text
     13.4:1, muted 5.0:1, red 5.1:1, all clear of AA. */
  --paper:      #f7f2e7;
  --paper-warm: #fdfbf5;
  --card:       #ffffff;
  --rule:       #e4e3df;
  --rule-soft:  #efeeea;
  --red:        #b52426;
  --red-wash:   #f5e3e3;
  --muted:      #5f5f66;
  --muted-dark: #a2a2aa;

  /* The two families the Stitch design specifies, self-hosted above.
     The fallbacks only matter if a woff2 fails to load. */
  --serif: "Noto Serif", Georgia, "Times New Roman", serif;
  --sans:  "Hanken Grotesk", system-ui, -apple-system, "Segoe UI", Roboto,
           Helvetica, Arial, sans-serif;
  --brand: "Yama Arashi", var(--serif);

  /* Header wordmark. clamp() so it does not overflow a phone: 30px at
     narrow widths, up to 42px on a desktop masthead. */
  --wordmark-size: clamp(30px, 4.2vw, 42px);
  --wordmark-weight: 700;   /* synthetic: the face has no bold cut */
  --wordmark-stroke: 0.6px; /* real thickening on top of the synthesis */
  /* Drops the wordmark and its sub-line together, relative to the crest.
     The blackletter's optical centre sits high in its line box, so grid
     centring alone leaves it looking lifted against the circle. Both lines
     move by the same amount, so the gap set by --logo-sub's margin is
     preserved. */
  --logo-text-drop: 7px;
  /* Phone only. Nudges the crest and wordmark -- row 1 of the logo grid --
     without moving the characters or the sub-line beneath them, which sit in
     their own rows and are centred there. */
  --logo-row1-shift: -4px;
  /* Drops the crest against the wordmark. The wordmark already sits
     --logo-text-drop down; without this the disc rides high beside it. */
  --logo-seal-drop: 5px;

  /* Relief on the school face. It ships Regular only and reads flat at
     display sizes; these lift it off the paper. The white rim sits above the
     glyph and the dark shadow below-right, which is what "raised" means to
     an eye. Keep the blur small -- this is blackletter, and anything over
     about 2px starts creeping into the counters of "a" and "m".
     Set both to `none` for the flat face. */
  --brand-relief: 0 -1px 0 rgb(255 255 255 / .80), 1px 2px 1.5px rgb(0 0 0 / .32);
  --brand-relief-dark: 0 1px 0 rgb(0 0 0 / .55), 0 2px 5px rgb(0 0 0 / .45);

  --wrap: 1180px;
  --gutter: 24px;
}

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

/* `only light`, not `light`. Plain `light` says the page supports a light
   scheme; `only light` says it supports nothing else, which is what actually
   stops a user agent applying its own dark transformation. See the note in
   site/layout.html for why there is no dark variant to offer. */
html { -webkit-text-size-adjust: 100%; color-scheme: only light; }

body {
  margin: 0;
  background: var(--paper-warm);
  color: var(--text);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.6;
}

img { max-width: 100%; display: block; }

a { color: inherit; }

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

/* The school font. Mixed case only, 24px and up -- see the note above. */
/* .h-brand: a heading set ENTIRELY in the school font.
   Two rules come with it, both forced by the face itself:

   1. text-transform is reset to none. The other section headings are
      all-caps; blackletter capitals lose the ascenders that tell them
      apart, so "THE CLASSICAL ARTS" in this font is a picket fence.
      Mixed case is the only way it reads.
   2. Extra letter-spacing. The vertical strokes are dense and collide
      at default tracking.

   Mixing the school font with the serif inside one line looked wrong --
   two faces in four words. So a heading is now all one or all the other. */
.h-brand {
  font-family: var(--brand);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0.025em;
  line-height: 1.18;
  text-shadow: var(--brand-relief);
}
/* The face has no bold cut -- only Regular, 100 glyphs. Where weight is
   wanted the browser synthesises it, which on blackletter thickens the
   stems without muddying them at display sizes. Used on the wordmark. */
.brand-inline { text-transform: none; }

.brand {
  font-family: var(--brand);
  font-weight: 400;
  letter-spacing: 0.02em;
}

h1, h2, h3 { font-family: var(--serif); font-weight: 700; line-height: 1.12;
             letter-spacing: 0.005em; margin: 0; }

/* The template sets every section heading in all-caps serif. */
.h-section { font-size: clamp(30px, 4.4vw, 46px); text-transform: uppercase; }

/* .h-brand has to come AFTER .h-section: same specificity, so the later
   rule wins. Without this line every school-font heading inherits
   .h-section's uppercasing and renders as all-caps blackletter, which is
   the one thing this face genuinely cannot do. */
.h-section.h-brand { text-transform: none; }

.kicker {
  font-size: 11px; font-weight: 700; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--red);
  display: flex; align-items: center; gap: 9px; margin: 0 0 14px;
}
.kicker::before { content: ""; width: 9px; height: 9px; background: var(--red); flex: none; }
.kicker.dash::before { width: 18px; height: 2px; }

.lede { color: var(--muted); max-width: 52ch; margin: 0; }

.skip { position: absolute; left: -9999px; }
.skip:focus {
  left: 16px; top: 16px; z-index: 100; background: var(--ink); color: #fff;
  padding: 12px 18px; text-decoration: none;
}

/* ---------------- buttons ---------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  font-family: var(--sans); font-size: 12px; font-weight: 700;
  letter-spacing: 0.11em; text-transform: uppercase;
  padding: 15px 26px; border: 0; text-decoration: none; cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
.btn-red   { background: var(--red); color: #fff; }
.btn-red:hover   { background: #9d1f21; }
.btn-dark  { background: var(--ink); color: #fff; }
.btn-dark:hover  { background: #000; }
.btn-ghost { background: rgba(255,255,255,.12); color: #fff;
             border: 1px solid rgba(255,255,255,.22); }
.btn-ghost:hover { background: rgba(255,255,255,.2); }
.btn-outline { background: transparent; color: var(--ink); border: 1px solid var(--rule); }
.btn-outline:hover { background: var(--ink); color: #fff; }
.btn-block { width: 100%; }

/* ---------------- header ---------------- */
.masthead { background: transparent; border-bottom: 1px solid var(--rule); }
.masthead .wrap {
  display: flex; align-items: center; gap: 12px 18px;
  min-height: 94px; flex-wrap: wrap;
}
/* When the bar is too narrow for one row, the NAV takes its own full-width
   row and the "Free First Month" button stays up beside the wordmark. The
   default flex wrap orphaned the button onto a line of its own, which is
   what it did once the wordmark grew. */
.masthead .mainnav { order: 3; flex: 1 0 100%; }
.masthead .btn { order: 2; flex: none; white-space: nowrap; }
/* One row now needs 1099px of content: wordmark 351, kanji 136, nav 366,
   button 174 and four gaps. The three characters cost 136px plus a gap, so
   this went back up from 1020px -- below 1180px the nav takes its own row
   rather than the bar overflowing. */
@media (min-width: 1180px) {
  .masthead .mainnav { order: 0; flex: 0 1 auto; margin-left: auto; }
}

/* The three characters, between the wordmark and the nav. The wordmark
   carries margin-right:auto and the nav margin-left:auto, so the two autos
   split the free space evenly and drop this in the middle of the gap. */
.logo-kanji { height: 34px; width: auto; flex: none; }

/* Below the desktop breakpoint the nav drops to its own row, and the
   characters stay inline beside the wordmark while there is room. */
/* Below that the nav has its own row, so centre it. Left in the markup it
   inherited the push from the wordmark's auto margin and sat hard right,
   under a left-aligned wordmark, which reads as a mistake rather than a
   choice. */
@media (max-width: 1179px) {
  .masthead .mainnav { justify-content: center; }
}

/* Two rows, two columns. The crest occupies row 1 alongside the wordmark
   and is centred against it, so the two line up on one optical axis. The
   sub-line is row 2, which puts "Denver Budo Dojo" just below the bottom of
   the circle rather than sharing its centre.

   The old flex version centred the whole text block -- wordmark AND
   sub-line -- against the circle, so neither lined up with it. */
.logo {
  display: grid;
  grid-template-columns: auto auto;
  column-gap: 14px;
  align-items: center;
  text-decoration: none;
  margin-right: auto;
  flex: none;
}
.logo-seal {
  width: 54px; height: 54px; border-radius: 50%;
  grid-column: 1; grid-row: 1;
  position: relative; top: var(--logo-seal-drop);
}
.logo-name, .logo-name-img { grid-column: 2; grid-row: 1; position: relative; top: var(--logo-text-drop); }
/* The wordmark as artwork rather than type. It keeps the same --wordmark-size
   measure so the masthead still reflows the way it did, but everything the
   live text had -- synthetic bold, --wordmark-stroke, selectable text -- no
   longer applies. Swap the <img> back for the <span class="logo-name"> in
   site/layout.html to restore it. */
/* The multiplier IS the artwork's aspect ratio, which is what keeps the
   rendered HEIGHT equal to --wordmark-size. Re-cut the wordmark and this
   number has to move with it: the 2026-09-25 version tightened the word
   spacing, which took the ratio from 7.17 to 6.89. Leave it at the old value
   and the letters simply come out taller. */
.logo-name-img { width: calc(var(--wordmark-size) * 6.89); height: auto; align-self: center; }
.logo-sub  { grid-column: 2; grid-row: 2; position: relative; top: var(--logo-text-drop); }
/* Wordmark -- the school font, mixed case. Size, weight and stroke live
   in the variables at the top of the file so they are easy to dial.

   The face ships Regular only, so font-weight:700 is browser-synthesised
   bold. On blackletter that alone is subtle, so a hairline text-stroke
   does the rest: it thickens the stems for real rather than smearing
   them. 0.6px is the ceiling -- at 1.0px the counters in "a" and "m"
   start filling in. Set --wordmark-stroke to 0 for the plain weight. */
.logo-name {
  font-family: var(--brand);
  font-size: var(--wordmark-size);
  font-weight: var(--wordmark-weight);
  -webkit-text-stroke: var(--wordmark-stroke) currentColor;
  line-height: 1.05;
  letter-spacing: 0.025em;
  text-shadow: var(--brand-relief);
}
/* The three characters beside the wordmark are a PNG, not the font, so they
   need the same lift by a different route or the masthead reads half-raised.
   drop-shadow follows the glyph edges; box-shadow would draw a rectangle. */
.logo-kanji { filter: drop-shadow(1px 2px 1.5px rgb(0 0 0 / .30)); }
/* Tucked up under the wordmark's descenders. The blackletter leaves a lot
   of air below its baseline, so without this the sub-line drifts too far
   from the circle. */
.logo-sub  { font-size: 10.5px; font-weight: 700; letter-spacing: 0.17em;
             text-transform: uppercase; color: var(--red); margin-top: -4px; }

/* Only one of the two copies is ever shown. */
.logo-kanji-stacked { display: none; }

/* Every phone override for the masthead lives here, AFTER the base rules it
   overrides. A media query changes when a rule applies, not how strongly, so
   an override placed earlier in the file simply loses. That mistake produced
   an overlapping logo and a logo jammed against the left edge; both looked
   like layout bugs and were ordering bugs. */
@media (max-width: 719px) {
  /* the wordmark alone fills the bar, so button and nav each take a row */
  .masthead .wrap { justify-content: center; }
  .logo { margin-right: 0; }          /* or the auto margin drags it left */
  .masthead .btn { flex: 1 0 100%; }  /* full-width target for a thumb */

  .logo-kanji-inline { display: none; }

  /* Inside the logo grid, so it can sit BETWEEN the roman name and "Denver
     Budo Dojo" -- row 2, spanning both columns, which centres it across the
     crest and wordmark together. The sub-line moves down to row 3.
     "Denver Budo Dojo" previously sat in column 2 only, under the wordmark
     and therefore offset right by the width of the crest: centred within the
     logo, but not on the screen, which is how it looked. */
  .logo-kanji-stacked {
    display: block; grid-column: 1 / -1; grid-row: 2;
    justify-self: center;
    height: 30px; width: auto; margin: 6px 0 2px;
  }
  .logo-sub { grid-column: 1 / -1; grid-row: 3; justify-self: center;
              text-align: center; }

  /* No vertical nudge on the phone any more -- only the horizontal shift,
     which moves row 1 alone so the crest and wordmark sit together above the
     characters and the sub-line.
     This used to add 2px, from when the wordmark was live text: the crest
     read as riding high beside it, so it was brought down to meet it. The
     wordmark is an IMAGE now and sits differently in its box, so that nudge
     left the crest 2px low instead. Astone's eye, 2026-09-22. */
  .logo-seal { top: var(--logo-seal-drop); left: var(--logo-row1-shift); }
  .logo-name { left: var(--logo-row1-shift); }

  /* The wordmark fills the bar instead of being derived from
     --wordmark-size. That derivation is a fixed 216px once the clamp bottoms
     out at 30px, which is too small on a 390px phone and actually OVERFLOWS
     a 320px one -- 216px of image against 204px of room.
     The 116px is what the row costs beside it: 48px of .wrap gutter, the
     54px crest and the 14px grid gap. The cap stops it ballooning on a
     600px-wide phone, where this query still applies. */
  .logo-name-img { width: min(calc(100vw - 116px), 360px); }
}


.mainnav { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; }
.mainnav a {
  font-size: 11.5px; font-weight: 700; letter-spacing: 0.09em;
  text-transform: uppercase; text-decoration: none; color: var(--muted);
  padding: 9px 11px; white-space: nowrap;
}
.mainnav a:hover { color: var(--ink); }
.mainnav a[aria-current="page"] { background: var(--ink); color: #fff; }

/* ---------------- hero ---------------- */
.hero {
  position: relative; background: var(--ink-3); color: #fff;
  padding: 96px 0 0;
  /* The dojo-interior still from the Stitch project. It is only 512px on
     the long edge -- the largest Stitch holds -- so it is deliberately
     scrimmed heavily: a dark overlay hides softness that would show on a
     wide screen, and the design calls for a dark hero anyway. Replace
     with a real photograph of the dojo when one exists and the scrim can
     be lightened. */
  background-image:
    linear-gradient(180deg, rgba(18,18,20,.62) 0%, rgba(18,18,20,.84) 100%),
    url('/img/hero-dojo.jpg');
  background-size: cover;
  background-position: center 32%;
}
.hero-badge {
  display: inline-flex; align-items: center; gap: 10px;
  background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.12);
  padding: 9px 16px; margin-bottom: 26px;
  font-size: 11px; font-weight: 700; letter-spacing: 0.13em; text-transform: uppercase;
}
.hero-badge::before { content: ""; width: 8px; height: 8px; background: var(--red); flex: none; }
.hero-badge i { font-style: italic; font-family: var(--serif); color: var(--red);
                text-transform: none; letter-spacing: 0.02em; font-size: 13px; font-weight: 400; }
.hero h1 {
  font-size: clamp(40px, 7vw, 74px); line-height: 1.04;
  text-transform: uppercase; max-width: 15ch; margin: 0 0 22px;
}
.hero h1 em { font-style: italic; text-transform: none; }
.hero p { color: #c9c9d0; max-width: 46ch; font-size: 17px; margin: 0 0 32px; }
.hero-cta { display: flex; gap: 14px; flex-wrap: wrap; padding-bottom: 54px; }

.trustbar {
  border-top: 1px solid rgba(255,255,255,.1);
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 22px;
  padding: 26px 0 34px;
}
.trust { display: flex; gap: 11px; }
.trust-mark { color: var(--red); flex: none; line-height: 1; font-size: 15px; }
.trust b { display: block; font-size: 11.5px; letter-spacing: 0.1em; text-transform: uppercase; }
.trust span { font-size: 12.5px; color: var(--muted-dark); }

/* ---------------- generic section ---------------- */
.section { padding: 78px 0; }
/* Transparent on purpose -- see the paper texture note below. These
   full-width bands used to paint their own colour, which restarted the
   texture tile at every boundary; the shift was visible.

   The alternation is now done the other way round. The SHEET is the lighter
   tone (--paper-warm, on body) and carries the texture at full strength;
   the deeper tone is a thin wash laid over it. The first attempt did the
   reverse -- deeper sheet, 30% WHITE over it for the light bands -- and 30%
   white bleaches the texture as well as the colour, so those bands went pale
   AND flat. That is what read as "too white".

   rgb(193 161 105 / .10) over #fdfbf5 composites to exactly #f7f2e7, and
   keeps 90% of the texture. Solved, not eyeballed: any wash that lifts blue
   toward white needs ~53% alpha to move it that far, whereas tinting DOWN
   to the same pair of tones needs only 10%. The wash follows from the two
   tones rather than being picked: for a 10% overlay, wash = 10*dark - 9*light,
   channel by channel. Change --paper or --paper-warm and recompute it, or
   the alternation stops being two tones of one sheet. */
.section-warm  { background-color: transparent; }
.section-paper { background-color: rgb(193 161 105 / .10); }
.section-dark { background: var(--ink); color: #fff; }

/* ---------------- paper texture ----------------
   Real crumpled paper, not synthesised. Six attempts at generating one
   failed -- smooth noise reads as drywall, faceted noise reads as cracked
   mud -- and the honest lesson is that photographed paper has a structure
   that fractal noise does not.

   Source: paper.png on ktulu, which arrived as a SCREENSHOT with the
   editor's transparency checkerboard baked in. Recovered rather than
   re-shot: a checker is a square wave, so its energy sits only on the
   odd-harmonic lattice points of the 2D spectrum -- (+/-k*13.3, +/-m*20.05)
   for odd k, m at this scale. Notching those 256 points removes the grid and
   keeps 93% of the paper detail. (Notching every strong peak instead, which
   was the first try, removes the high frequencies and turns it to porridge.)
   The de-gridded full-size copy is paper-clean.png, beside the original.

   Then: an 800px square crop, edges cross-faded against their opposites so
   the tile wraps, and remapped to a mostly-white MULTIPLY tile. The gamma
   matters -- without it the whole sheet darkens evenly instead of only the
   creases.

   It darkens: --paper composites to 233, about 5%. That is a real change to
   a palette design/README.md says was sampled rather than eyeballed. Lower
   DEPTH in the generator to soften it, or accept the warmer sheet.

   PROVENANCE IS UNCONFIRMED. Nobody has said where paper.png came from. If
   it is stock or off the internet it belongs in the same bucket as the four
   uncredited charts on /documents/ -- useful, kept, and not clearly ours to
   publish. Settle that before this goes public.

   It lives on <body> ALONE and the full-width light bands above are
   transparent. That is the whole trick: a background belongs to the element
   that paints it, so when each section painted its own the tile restarted at
   every boundary and the seam showed as a shift. One sheet on body, scrolling
   with the document, is continuous the length of the page. Astone spotted the
   shift and named the fix.

   Inset components -- cards, tables, the pledge panel -- keep their own
   opaque backgrounds. They are meant to read as separate surfaces laid on
   the sheet, so restarting there is correct.

   Dark sections get none of this: they read as ink, and graining them made
   #karate-lineage look filthy. .pagehead and .hero carry photographs. */
body {
  background-image: url('/img/paper-crumple.jpg');
  background-size: 690px 690px;
  background-repeat: repeat;
  background-blend-mode: multiply;
  /* Scrolls with the content, which is the default.
     `background-attachment: fixed` was tried and reverted: it pins the sheet
     to the VIEWPORT, so the texture sits dead still while the words slide
     over it. That is the maximum of the effect, not a mild version of it,
     and it reads as wrong.
     True parallax -- the texture moving, but slower than the content -- is
     not reachable from background-attachment, which only offers `scroll` and
     `fixed`. It needs either JavaScript, which this repo does not have, or
     the perspective/translateZ trick, which turns the page into a scrolling
     container and would put the :target biography panels and the fixed
     #closed anchor at risk. Worth discussing in person before anyone tries
     it. */
}

/* ---------------- second paper ----------------
   A different photographed sheet -- softer, larger creases -- wrapped around
   the content of every page. Tried on /schedule/ alone first, then rolled
   out on Astone's word, 2026-09-24. The masthead, the page heads and the
   footer are outside every wrapper and keep the site-wide sheet: "It should
   leave the header and footers of the page untouched. I like the way they
   look."

   /documents/ was held back then, on Astone's instruction that day, and
   joined the rest on 2026-10-05 at his word: "make the page's background on
   the Documents page match the rest of the site". It was the only page
   showing body's paper-crumple through its own transparent sections, and
   beside the others that grittier, higher-contrast sheet read as a different
   page rather than a deliberate one.

   Two things this rule is doing, both deliberate:

   - It paints an OPAQUE colour, which the section bands themselves do not.
     Without that, `body`'s texture shows through the transparent sections
     and the two papers stack.
   - It is on one wrapper per page rather than on each section, so the tile
     runs continuously the whole way down. Per-section backgrounds restart
     the tile at every boundary, which is the visible shift the note above
     .section-warm records. Dark sections inside a wrapper paint over it --
     they read as ink, and graining them made #karate-lineage look filthy --
     and because the wrapper is one element the tile stays in phase
     underneath them and picks up correctly on the far side.

   The sections inside keep their own colours untouched -- .section-warm
   transparent, .section-paper the same 10% wash -- so the light/dark
   alternation is exactly as it was and the only thing that changed is the
   paper under it. The yellow itself is a separate question Astone has
   parked.

   Source and the two steps that make a photograph tile (flatten the lighting,
   cross-fade the edges) are in scripts/build-texture.py.

   `background-size` is 820px against an 1163px file ON PURPOSE. Astone,
   2026-09-24: "the paper looks like it's lower res than the image." It did.
   The first version was an 820px tile rendered at 820px, so on a HiDPI screen
   it was the one thing on the page the browser had to upscale -- every other
   asset here ships at 2x. JPEG was not the cause; at this contrast q82 keeps
   98% of the fine grain, which was measured rather than assumed.

   So the tile is now as large as the source photograph allows and the render
   size is unchanged, which buys 1.42x device pixels without touching the
   crease scale Astone approved. It does not reach 2x: density is tile px over
   css px, so 2x at this crease scale needs a source 1790px on its short side
   and this one is 1313. Shrinking background-size would get to 2x by making
   every crease smaller, which is a different look, not a sharper one. */
.paper-crease {
  background-color: var(--paper-warm);
  background-image: url('/img/paper-crease.jpg');
  background-size: 820px 820px;
  background-repeat: repeat;
  background-blend-mode: multiply;
}

@media print {
  body, .paper-crease { background-image: none; }
}

.section-dark .lede { color: var(--muted-dark); }
/* On ink the glyph is the light thing, so a white rim above it makes no
   sense -- it just fogs the letter. Drop shadow only. */
.section-dark .h-brand, .pagehead .h-brand { text-shadow: var(--brand-relief-dark); }

.section-head { display: grid; gap: 18px; margin-bottom: 42px; }
@media (min-width: 900px) {
  .section-head.split { grid-template-columns: 1fr 1fr; align-items: end; }
}
.section-head.center { text-align: center; justify-items: center; }
.section-head.center .kicker { justify-content: center; }
.section-head.center .lede { max-width: 58ch; }

/* ---------------- why: numbered cards ---------------- */
.why-grid { display: grid; gap: 22px; grid-template-columns: 1fr; }
@media (min-width: 760px)  { .why-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .why-grid { grid-template-columns: repeat(3, 1fr); } }
/* Four instructors in a three-column grid leaves the fourth card alone at
   the far left of a second row -- Astone, 2026-09-24, saw it as Adam
   "sitting far left". Putting it in the middle column centres it under the
   three above.

   `:nth-child(4):last-child` means this only fires when there are exactly
   four cards. A fifth instructor makes it stop applying by itself, which is
   the point: 3 + 2 wants the pair left-aligned, not one of them shoved into
   the middle. The home page's why-grid has three cards and is untouched
   either way. */
@media (min-width: 1000px) {
  #instructors .why-card:nth-child(4):last-child { grid-column: 2; }
}

.why-card { background: var(--card); border-top: 3px solid var(--red); display: flex; flex-direction: column; }
.why-card.alt { border-top-color: var(--ink); }
.why-card-body { padding: 26px 26px 20px; flex: 1; }
.why-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
.why-num { font-family: var(--serif); font-size: 26px; color: var(--red); }
.why-card.alt .why-num { color: var(--ink); }
.why-icon { color: #b9b9bf; font-size: 17px; line-height: 1; }
.why-card h3 { font-size: 21px; text-transform: uppercase; margin-bottom: 12px; }
.why-card p { margin: 0; color: var(--muted); font-size: 14.5px; }
.why-foot { border-top: 1px solid var(--rule-soft); padding: 18px 26px 24px; background: var(--paper-warm); }
.why-foot b { display: block; font-size: 10px; letter-spacing: 0.14em;
              text-transform: uppercase; color: var(--muted); margin-bottom: 5px; }
.why-foot i { font-family: var(--serif); font-size: 14.5px; }

/* ---------------- disciplines ---------------- */
.disc-grid { display: grid; gap: 20px; grid-template-columns: 1fr; }
@media (min-width: 680px)  { .disc-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) { .disc-grid { grid-template-columns: repeat(4, 1fr); } }

.disc { background: var(--paper); padding: 18px; display: flex; flex-direction: column; }
.disc-media { position: relative; margin-bottom: 18px; }
/* THE HEIGHT IS IN CSS BECAUSE IT HAS TO BE. This rule used to say
   `aspect-ratio: 4 / 3`, and that was inert: `aspect-ratio` only applies when
   a dimension is auto, and the markup's `height="384"` is a presentational
   hint that sets height for real. So the box has always been 384px tall and
   the 4:3 never applied -- which nobody saw, because all four photographs
   declared the same 384 and agreed with each other.

   It stops being invisible the moment one image declares something else. The
   Jujitsu drawing declares its true 840x1018 and the card grew to 1018px
   tall, pushing its title half a screen below the other three.

   It was 384px, which is what the photographs' own `height` attribute had
   always produced. 240px since 2026-10-02, astone's call: every card is a
   drawing now, drawings are `contain` rather than `cover`, and not one of the
   four is taller than 215px in a 232px-wide card. The other 170-ish pixels
   were empty paper above and below each one, four times over.

   240 is a ceiling, not a crop. All four plates are wider than the card's
   0.97:1 box, so each is limited by width and clears the height with room to
   spare -- the tallest, Kid's Karate, lands at 215. A plate TALLER than the
   box would be scaled down to fit it rather than cut, which is what `contain`
   buys, but it would also come out small. Check a new one here. */
.disc-media img {
  width: 100%; height: 240px; object-fit: cover; background: #dedbd4;
}
/* A DRAWING in a discipline card, as against a photograph.

   The rule above crops a photograph to fill the 4:3 box. Line art on
   transparency cannot take that: filling the box cuts the top and bottom off
   the figure, and the transparent ground comes up as the grey fill. So the
   whole drawing is fitted inside the same box instead, the fill goes, and it
   multiplies onto the card's paper -- the same reasoning as .art-ink on
   /disciplines/, written out there.

   Specificity, not order, is what makes this beat the rule above: two classes
   against one class and an element.

   THE BOX STAYS 4:3 ON PURPOSE, for now. It keeps the four cards the same
   height and their titles on one line. It also leaves a portrait drawing
   small in a landscape box, which is a real cost. Astone is drawing the
   other three disciplines (2026-10-02); when they land, the box shape is one
   decision to make across all four rather than four times. */
.disc-media .disc-ink {
  object-fit: contain; background: none; mix-blend-mode: multiply;
}
.disc h3 { font-size: 20px; text-transform: uppercase; margin-bottom: 10px; }
.disc p { color: var(--muted); font-size: 14px; margin: 0 0 18px; }
/* `margin-top: auto` HERE is what lines the white boxes up across the row.

   The card is a flex column and the stuff above this varies: a title that
   wraps to two lines on Jujitsu and Karate but not the others, an .art-ryu
   line that Kid's Karate has not got, and a description between three and
   four lines. So the box landed at a different height in all four cards.

   An auto top margin eats the leftover space, so this block and the button
   after it sit at the bottom. The cards are grid items and stretch to a
   common height, and the box and the button are the same height in every
   card, so bottom-aligning them aligns them with each other.

   The button used to carry this instead, which is why the buttons already
   lined up and the boxes did not. It cannot be on BOTH: multiple auto
   margins SHARE the free space rather than each taking it, so the boxes
   would stay staggered and the gap above the buttons would open up.

   If a card ever ships without a .disc-meta, this has to move to whatever is
   first in that bottom group, or its button stops reaching the bottom. */
.disc-meta { background: var(--card); padding: 12px 14px; margin-top: auto;
             margin-bottom: 16px; font-size: 12px; }
.disc-meta div { display: flex; justify-content: space-between; gap: 12px; padding: 3px 0; }
.disc-meta dt { color: var(--muted); margin: 0; }
.disc-meta dd { margin: 0; font-weight: 700; }
.disc-meta dd.red { color: var(--red); }

/* ---------------- kuzushi stepper (CSS-only) ---------------- */
.kz { display: grid; gap: 20px; grid-template-columns: 1fr; }
.kz-left { display: flex; flex-direction: column; gap: 10px; }
@media (min-width: 940px) { .kz { grid-template-columns: 330px 1fr; } }

.kz-head { display: grid; gap: 16px; margin-bottom: 36px; }
@media (min-width: 940px) { .kz-head { grid-template-columns: 1fr auto; align-items: center; } }
.kz-principle {
  background: var(--ink-2); padding: 12px 18px; font-family: var(--serif);
  font-size: 13.5px; font-style: italic; color: #d6d6dc;
}

.kz-steps { display: flex; flex-direction: column; gap: 10px; }
.kz-steps label {
  display: flex; gap: 14px; align-items: flex-start;
  background: var(--ink-2); padding: 17px 20px; cursor: pointer;
  border: 1px solid transparent; transition: background .15s ease;
}
.kz-steps label:hover { background: #3a3a3f; }
.kz-steps label span.n { font-family: var(--serif); font-size: 15px; color: var(--muted-dark); }
.kz-steps label b { display: block; font-size: 14.5px; letter-spacing: 0.04em; }
.kz-steps label i { font-style: normal; font-size: 12.5px; color: var(--muted-dark); }

.kz input { position: absolute; opacity: 0; pointer-events: none; }
.kz input:focus-visible + label { outline: 2px solid #fff; outline-offset: 2px; }

.kz-panel { background: var(--ink-2); padding: 28px; display: none; }
.kz-meta { display: flex; justify-content: space-between; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
.kz-step-no { font-size: 10.5px; font-weight: 700; letter-spacing: 0.15em;
              text-transform: uppercase; color: var(--red); }
.kz-caption { font-family: var(--serif); font-style: italic; font-size: 13.5px; color: var(--muted-dark); }
.kz-panel h3 { font-size: clamp(22px, 3vw, 31px); text-transform: uppercase; margin-bottom: 14px; }
.kz-panel > p { color: #c9c9d0; font-size: 15px; margin: 0 0 22px; max-width: 62ch; }
.kz-facts { background: var(--ink-3); padding: 18px; display: grid; gap: 16px; grid-template-columns: 1fr; }
@media (min-width: 620px) { .kz-facts { grid-template-columns: repeat(3, 1fr); } }
.kz-fact { display: flex; gap: 10px; }
.kz-fact span { color: var(--red); flex: none; font-size: 14px; line-height: 1.2; }
.kz-fact b { display: block; font-size: 10.5px; letter-spacing: 0.11em; text-transform: uppercase; }
.kz-fact i { font-style: normal; font-size: 12px; color: var(--muted-dark); }
.kz-foot { display: flex; justify-content: space-between; align-items: center;
           gap: 16px; margin-top: 20px; font-size: 13px; color: var(--muted-dark); }

/* Radio N reveals panel N. Descendant combinators, so the left column can
   hold the safety note alongside the step labels. */
#kz1:checked ~ .kz .kz-panel:nth-of-type(1),
#kz2:checked ~ .kz .kz-panel:nth-of-type(2),
#kz3:checked ~ .kz .kz-panel:nth-of-type(3) { display: block; }

#kz1:checked ~ .kz label[for="kz1"],
#kz2:checked ~ .kz label[for="kz2"],
#kz3:checked ~ .kz label[for="kz3"] { background: var(--red); }

#kz1:checked ~ .kz label[for="kz1"] span.n,
#kz2:checked ~ .kz label[for="kz2"] span.n,
#kz3:checked ~ .kz label[for="kz3"] span.n,
#kz1:checked ~ .kz label[for="kz1"] i,
#kz2:checked ~ .kz label[for="kz2"] i,
#kz3:checked ~ .kz label[for="kz3"] i { color: #f2d2d2; }

.safety { background: var(--ink-2); padding: 18px; display: flex; gap: 14px; margin-top: 10px; }
.safety-mark { background: var(--red); width: 42px; height: 42px; flex: none;
               display: grid; place-items: center; color: #fff; font-size: 17px; }
.safety b { display: block; font-size: 11px; letter-spacing: 0.12em;
            text-transform: uppercase; margin-bottom: 5px; }
.safety p { margin: 0; font-size: 12.5px; color: var(--muted-dark); }

/* ---------------- schedule ---------------- */
.legend { display: flex; gap: 20px; flex-wrap: wrap; }
.legend span { display: flex; align-items: center; gap: 8px; font-size: 11px;
               font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; }
.legend i { width: 9px; height: 9px; display: block; }
.legend i.ev { background: var(--red); }
.legend i.mo { background: var(--ink); }

.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table.schedule { width: 100%; min-width: 720px; border-collapse: collapse; background: var(--card); }

/* The repeated day. Hidden rather than removed, so the column keeps its
   width and the table reads like a merged cell -- but the text is still
   there for a screen reader and still there on a phone. */
.schedule .rep span { visibility: hidden; }
table.schedule caption { text-align: left; padding: 0 0 12px; font-size: 13px; color: var(--muted); }
table.schedule th, table.schedule td { padding: 17px 20px; text-align: left; vertical-align: middle; }
table.schedule thead th {
  background: var(--paper); font-size: 10.5px; font-weight: 700;
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--muted);
}
table.schedule tbody tr { border-top: 1px solid var(--rule-soft); }
table.schedule tbody tr:nth-child(even) { background: var(--paper-warm); }
table.schedule th[scope="row"] { font-family: var(--serif); font-size: 19px; font-weight: 700; }
table.schedule td { font-size: 14px; }
.cls { display: inline-block; }
.cls-red  { background: var(--red-wash); color: var(--red); font-weight: 600; padding: 5px 11px; }
.cls-mo   { background: var(--rule-soft); color: var(--ink); font-weight: 600; padding: 5px 11px; }
.cls-em   { font-style: italic; color: var(--muted); }
.cls-b    { font-weight: 700; }
.t-red { color: var(--red); font-weight: 700; }

/* ---------------- the schedule on a phone ----------------
   A 720px table in a 312px viewport scrolls sideways, and nothing on screen
   says so -- the times were simply invisible to anyone who did not think to
   swipe. Below 760px each row becomes a card instead: day, class, time,
   stacked. No horizontal scroll at any width, and nothing hidden. */
@media (max-width: 760px) {
  .table-scroll { overflow-x: visible; }
  table.schedule { min-width: 0; display: block; background: none; }
  /* A caption inside a display:block table keeps display:table-caption and
     shrink-wraps to its content, so the line bunched against the left edge
     instead of setting across the column. */
  table.schedule caption { display: block; width: 100%; padding-bottom: 16px; }
  table.schedule thead { position: absolute; left: -9999px; }   /* headers are in the cards */
  table.schedule tbody { display: block; }

  table.schedule tbody tr {
    display: block; background: var(--card) !important;
    border: 1px solid var(--rule-soft); border-left: 3px solid var(--red);
    padding: 14px 16px; margin-bottom: 10px;
  }
  table.schedule th[scope="row"], table.schedule td { display: block; padding: 0; }

  /* the day leads the card, and the repeat is shown again here */
  table.schedule th[scope="row"] {
    font-family: var(--sans); font-size: 11px; font-weight: 700;
    letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted);
    margin-bottom: 5px;
  }
  .schedule .rep span { visibility: visible; }

  table.schedule td:nth-of-type(1) { font-size: 16px; margin-bottom: 3px; }
  table.schedule td:nth-of-type(2) { font-size: 15px; }
  .schedule .cls-red, .schedule .cls-mo { padding: 0; background: none; }
  .schedule .cls-red { color: var(--red); font-weight: 700; }
  .schedule .cls-mo  { color: var(--ink); font-weight: 700; }
}

/* ---------------- event cards ---------------- */
.events { display: grid; gap: 20px; grid-template-columns: 1fr; margin-top: 26px; }
@media (min-width: 780px)  { .events { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) { .events { grid-template-columns: repeat(3, 1fr); } }

.event { background: var(--card); padding: 24px; display: flex; flex-direction: column; }
.event-top { display: flex; justify-content: space-between; align-items: center; gap: 14px; margin-bottom: 18px; }
.pill { font-size: 9.5px; font-weight: 700; letter-spacing: 0.13em;
        text-transform: uppercase; padding: 5px 10px; background: var(--red); color: #fff; }
.pill.dark { background: var(--ink); }
.pill.light { background: #fff; color: var(--ink); }
.event-date { font-family: var(--serif); font-style: italic; font-size: 13px; color: var(--muted); }
.event h3 { font-size: 21px; text-transform: uppercase; margin-bottom: 12px; }
.event p { color: var(--muted); font-size: 14px; margin: 0 0 20px; }
.event .btn { margin-top: auto; }
.event.red { background: var(--red); color: #fff; }
.event.red p { color: #f3d7d7; }
.event.red .event-date { color: #f3d7d7; }
.price { display: flex; align-items: baseline; justify-content: space-between;
         gap: 14px; border-top: 1px solid rgba(255,255,255,.28); padding-top: 18px; margin-top: auto; }
.price b { font-family: var(--serif); font-size: 38px; font-weight: 700; }
.price b small { font-size: 15px; font-weight: 400; }
.price span { font-size: 10px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }

/* ---------------- trial / pledge ---------------- */
.trial { background: var(--card); display: grid; gap: 0; grid-template-columns: 1fr; }
@media (min-width: 940px) { .trial { grid-template-columns: 1.25fr 1fr; } }
.trial-form { padding: 40px; }
.trial-form h2 { font-size: clamp(28px, 4vw, 40px); text-transform: uppercase; margin-bottom: 14px; }
.trial-form > p { color: var(--muted); font-size: 14.5px; max-width: 52ch; margin: 0 0 26px; }

.field-grid { display: grid; gap: 18px; grid-template-columns: 1fr; }
@media (min-width: 620px) { .field-grid { grid-template-columns: 1fr 1fr; } }
.field label { display: block; font-size: 10.5px; font-weight: 700;
               letter-spacing: 0.13em; text-transform: uppercase; margin-bottom: 7px; }
.field input, .field select {
  width: 100%; padding: 13px 14px; border: 1px solid var(--rule);
  background: var(--paper-warm); font: inherit; font-size: 14px; color: var(--ink);
}
.field input::placeholder { color: #a5a29b; }
.consent { display: flex; gap: 10px; align-items: flex-start; margin: 22px 0 24px; font-size: 12.5px; color: var(--muted); }
.consent input { margin-top: 3px; flex: none; }

.pledge { background: var(--paper); padding: 40px; position: relative; overflow: hidden; }
.pledge-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 18px; margin-bottom: 20px; }
.pledge-label {
  font-family: var(--brand);
  font-size: 21px;
  font-weight: 400;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--ink);
  line-height: 1.2;
}
.pledge-seal { width: 44px; height: 44px; border: 2px solid var(--red); color: var(--red);
               display: grid; place-items: center; font-size: 15px; flex: none; }
.pledge blockquote { margin: 0 0 18px; font-family: var(--serif); font-style: italic;
                     font-size: 16px; line-height: 1.55; }
.pledge > p { color: var(--muted); font-size: 14px; margin: 0; }
/* The samurai from the American Judo College's own information brochure,
   signed DS 64. Astone cut it off its red panel by hand on 2026-09-25 and
   that is the file shipped -- it is cleaner than the alpha mask computed
   here first, which carried the paper grain with it, and it keeps the warm
   near-black the ink picked up off the red. It sits behind the pledge as a
   watermark: the panel was already position:relative and overflow:hidden, so
   nothing else had to move for it.

   aria-hidden by construction -- a ::after with no content is not in the
   accessibility tree -- and pointer-events:none so it cannot eat a click on
   the text above it. Hidden on phones, where the panel is too narrow for the
   figure to read as anything but a smudge behind the words. */
.pledge::after {
  content: "";
  position: absolute; right: -14px; bottom: -10px;
  width: 190px; height: 350px;
  background: url('/img/samurai-1964.png') no-repeat bottom right;
  background-size: contain;
  opacity: .09;
  pointer-events: none;
}
@media (max-width: 620px) { .pledge::after { display: none; } }
.pledge-list { list-style: none; margin: 26px 0 0; padding: 22px 0 0; border-top: 1px solid var(--rule); }
.pledge-list li { display: flex; gap: 10px; align-items: center; font-size: 11px;
                  font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; padding: 5px 0; }
.pledge-list li::before { content: "\2713"; color: var(--red); font-weight: 700; }

/* ---------------- visit ---------------- */
.visit { display: grid; gap: 34px; grid-template-columns: 1fr; align-items: start; }
@media (min-width: 940px) { .visit { grid-template-columns: 1fr 1.15fr; } }
.visit h2 { margin-bottom: 16px; }
.visit > div > p { color: var(--muted); max-width: 46ch; }
.infolist { list-style: none; margin: 30px 0; padding: 0; display: grid; gap: 24px; }
.infolist li { display: flex; gap: 14px; }
.infolist .ico { color: var(--red); font-size: 15px; flex: none; line-height: 1.3; }
.infolist b { display: block; font-size: 15px; letter-spacing: 0.04em;
              text-transform: uppercase; margin-bottom: 4px; }
.infolist p { margin: 0; color: var(--muted); font-size: 14px; }

.map-ph { position: relative; border: 1px solid var(--rule); }
.map-ph img { width: 100%; aspect-ratio: 16 / 10; object-fit: cover; display: block; }
.map-card { position: absolute; left: 18px; bottom: 18px; background: var(--ink);
            color: #fff; padding: 14px 18px; max-width: 300px; text-align: left; }
.map-card b { display: block; font-size: 9.5px; font-weight: 700; letter-spacing: 0.14em;
              text-transform: uppercase; color: var(--red); margin-bottom: 5px; }
.map-card span { font-size: 14px; }
.map-card i { display: block; font-family: var(--serif); font-size: 12px;
              color: var(--muted-dark); margin-top: 4px; }

/* ---------------- footer ---------------- */
.sitefoot { background: transparent; border-top: 1px solid var(--rule); padding: 64px 0 0; }
.foot-grid { display: grid; gap: 34px; grid-template-columns: 1fr; }
@media (min-width: 700px)  { .foot-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1020px) { .foot-grid { grid-template-columns: repeat(4, 1fr); } }
.foot-h { font-size: 19px; text-transform: uppercase; margin-bottom: 18px;
          display: flex; align-items: center; gap: 9px; font-family: var(--serif); font-weight: 700; }
.foot-h::before { content: ""; width: 9px; height: 9px; background: var(--red); flex: none; }
/* Footer wordmark -- the school font again, mixed case. */
.foot-h.brand { font-size: 25px; font-weight: var(--wordmark-weight);
                text-transform: none; letter-spacing: 0.02em; }
.sitefoot p { color: var(--muted); font-size: 14px; margin: 0 0 12px; }
.foot-tag { display: flex; align-items: center; gap: 8px; font-size: 10.5px;
            font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
.foot-tag::before { content: ""; width: 8px; height: 8px; background: var(--red); flex: none; }
.hours { list-style: none; margin: 0; padding: 0; display: grid; gap: 11px; font-size: 13.5px; }
.hours li { display: flex; justify-content: space-between; gap: 14px; }
.hours b { font-weight: 700; }
.hours span { color: var(--muted); text-align: right; }
.hours span.red { color: var(--red); }
.lineage { font-family: var(--serif); font-style: italic; font-size: 14px; margin: 0 0 14px; }
.fineprint { font-size: 12px; color: var(--muted); }
.signup { display: flex; margin: 0 0 10px; }
.signup input { flex: 1; min-width: 0; padding: 12px 14px; border: 1px solid var(--rule);
                border-right: 0; background: var(--card); font: inherit; font-size: 13.5px; }
.signup button { border: 0; background: var(--ink); color: #fff; padding: 12px 20px;
                 font-size: 11px; font-weight: 700; letter-spacing: 0.11em;
                 text-transform: uppercase; cursor: pointer; }
.foot-bottom { border-top: 1px solid var(--rule); margin-top: 52px; padding: 24px 0 34px;
               display: flex; justify-content: space-between; gap: 18px; flex-wrap: wrap;
               font-size: 12.5px; color: var(--muted); }
.foot-bottom nav { display: flex; gap: 26px; flex-wrap: wrap; }
.foot-bottom a { font-size: 11px; font-weight: 700; letter-spacing: 0.11em;
                 text-transform: uppercase; text-decoration: none; }
.foot-bottom a:hover { color: var(--red); }

/* The call to action, in the one place on the page where there is no button
   to press. Marked in the accent so it does not read as the fifth of five
   equal links -- by the time someone reaches the footer, it is the only one
   that asks for anything. */
.foot-bottom a[href="/trial/"] { color: var(--red); }

/* Same fix as the masthead: space-between leaves the copyright and the links
   pushed to opposite edges, which on a phone means one line hard left and
   the next hard right. */
@media (max-width: 720px) {
  .foot-bottom { justify-content: center; text-align: center; }
  .foot-bottom nav { justify-content: center; gap: 12px 20px; }
}

/* ---------------- unwired notice ---------------- */
.unwired {
  background: #fff6d6; border: 1px solid #e3cf8a; color: #5c4a11;
  padding: 12px 16px; font-size: 12.5px; margin: 0 0 22px;
}
.unwired b { letter-spacing: 0.06em; text-transform: uppercase; font-size: 11px; }

@media (max-width: 760px) {
  .trustbar { grid-template-columns: repeat(2, 1fr); }
  .section { padding: 56px 0; }
  .trial-form, .pledge { padding: 28px 22px; }
}
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
}


/* ===================================================================
   Multi-page additions.

   The four nav items used to be anchors into one very long page. They
   are real pages now, so each needs a head, and the long page's
   sections were split across them.
   =================================================================== */

.pagehead {
  padding: 62px 0 58px;
  background-image:
    linear-gradient(180deg, rgba(18,18,20,.70) 0%, rgba(18,18,20,.88) 100%),
    url('/img/hero-dojo.jpg');
  background-size: cover;
  background-position: center 38%;
}
.pagehead h1 { font-size: clamp(34px, 6vw, 58px); margin: 0 0 16px; }
.pagehead .lede { color: #c9c9d0; max-width: 56ch; }

/* Astone, 2026-09-25: the kickers and headings stay left, the body text
   stays left-ALIGNED, but it runs the full width of the page rather than
   sitting in a 62ch column -- "the width of the tai chi chart".

   Worth knowing what that trades away: 62ch is roughly 75 characters, which
   is where typographic measure is usually set, and this is about 130 at a
   desktop width. Long lines are harder to track back to the start of the
   next one. If it reads tiring on a wide monitor, a max-width around 90ch
   is the middle ground -- one number, here. */
.prose { max-width: none; }
.prose p { margin: 0 0 1.1em; }
/* A single sentence set apart, for a line that is doing the work of a
   paragraph. Larger and given room above and below so it lands instead of
   scrolling past with everything else. */
.prose .lede-line {
  font-family: var(--serif);
  font-size: 21px; line-height: 1.45;
  margin: 1.5em 0 1.6em;
}
/* Figures between prose blocks centre on the page. The text does not: it
   stays ragged-right off a left margin, which is what Astone asked for. */
.prose > figure, .prose > .plate { margin-inline: auto; }

/* One art per row, image and text side by side, alternating. */
/* Three children in source order: the title block, the picture, then the
   body copy. That IS the phone layout -- Astone, 2026-09-25: on mobile the
   picture was sitting above the title, and he wants title, style, time and
   instructor first, then the art, then the rest of the words.

   On a desktop the same three are placed explicitly so nothing moves: the
   picture takes one column and spans both rows, the title block and the body
   stack in the other. `order` used to do the reversing; explicit placement
   replaces it, because order cannot express "this one spans two rows".

   row-gap is zero on desktop -- the 44px is between the columns. The title
   block carries its own margin to the body instead, or the heading would
   float a long way off its own text. */
.art { display: grid; gap: 30px; grid-template-columns: 1fr; align-items: start; }
.art-head { display: block; }
@media (min-width: 900px) {
  /* min-content on the first row matters. The picture spans both rows, and
     where it is taller than the words beside it -- a tall samurai against
     Kenjitsu's short copy -- the browser hands the slack to the rows it can
     grow. Without this, row one stretches and opens a gap between the title
     block and the first paragraph. Pinning row one to its content sends all
     the slack to row two, under the text, where nobody sees it. */
  .art { grid-template-columns: 420px 1fr; column-gap: 44px; row-gap: 0;
         grid-template-rows: min-content 1fr; }
  .art > .art-media { grid-column: 1; grid-row: 1 / span 2; }
  .art > .art-head  { grid-column: 2; grid-row: 1; }
  .art > .art-body  { grid-column: 2; grid-row: 2; }
  .art.reverse > .art-media { grid-column: 2; }
  .art.reverse > .art-head,
  .art.reverse > .art-body  { grid-column: 1; }
  .art-head { margin-bottom: 18px; }
}
.art-media { position: relative; }
.art-media img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; background: #dedbd4; }
/* A DRAWING IN A DISCIPLINE PLATE -- line art on transparency, laid straight
   on the sheet: no card, no frame, no grey fill.

   `mix-blend-mode: multiply` is the point of it. The ink darkens the paper
   rather than covering it, so the crumple texture reads THROUGH the drawing
   and it looks printed on the page instead of pasted over it.

   It also has to undo the rule directly above: that one crops an image to
   fill a 4:3 box, which on a standing figure slices him off at the knees.
   Selector is .art-media .art-ink, not .art-ink, because it has to outrank
   that rule rather than tie with it.

   This was .art-samurai, named for the one drawing on the page. On 2026-10-02
   astone redrew every discipline, so it is one class now and a new plate is a
   class attribute and nothing else. The caps are still the samurai's numbers,
   because he is the tallest thing that has to fit and what suits him suits
   the rest. */
.art-media .art-ink {
  aspect-ratio: auto; object-fit: contain; background: none;
  width: auto; max-width: 260px; height: auto;
  margin: 0 auto; mix-blend-mode: multiply;
}
/* HALF AGAIN AS BIG -- Karate and Tai Chi, astone 2026-10-02.

   The cap above is a max-WIDTH, which is the wrong handle for a wide
   drawing: at the same 414px the samurai stands 854px tall and Karate only
   307px. Same number, very different picture, and the two landscape plates
   came out looking small beside their own text.

   621px is 414 x 1.5, and it does not fit a 420px media column, so the
   column grows to 630 and the 1fr text column gives up the difference --
   about 458px at the full 1180px wrap, which is still a sane measure.

   On a phone there is no column to widen and they simply fill it. The 260px
   base cap exists for the TALL drawings, which would otherwise run past the
   bottom of the screen; a landscape plate at full phone width is about 254px
   tall and was never the thing that cap was protecting against. */
.art-media .art-ink-lg { max-width: 100%; }
/* The wide plates sit LEFT in their column, not centred.

   The base .art-ink centres with `margin: 0 auto`, which splits the leftover
   room either side. Both wide plates are .reverse, so their column is on the
   right of the page and that leftover lands between the words and the
   picture -- the gap astone wanted closed on 2026-10-02. Pushed left, the
   slack goes to the outside edge where it reads as page margin.

   It also has to happen before any plate is scaled DOWN: shrinking a centred
   image moves its left edge to the right, which is the opposite of the ask. */
.art-media .art-ink-lg { margin-left: 0; margin-right: auto; }
/* One knob per plate. --plate-full is the half-again size; --plate-scale
   trims an individual drawing off it without a second magic number to keep
   in step. Tai Chi is at .9 -- astone, 2026-10-02. */
.art-media .art-plate-90 { --plate-scale: .9; }
/* On a desktop he fills the whole 420px media column instead of sitting at
   300px in the middle of it -- Astone, 2026-09-25: "he is only 2/3 the size
   of the Yawara text next to him". At the column width the figure comes out
   about 744px tall. Filling the whole 420px column was tried and overshot:
   Astone, 2026-09-25, "his feet should not go beyond the bottom of the Yawara
   text -- don't count the note in yellow." The yellow note is not part of the
   measure. The 260px cap stays for the single-column layout, where filling
   the width would make him taller than the phone. */
@media (min-width: 900px) {
  /* min(100%, N) rather than a bare N. A px max-width does not make an image
     shrink to its container -- it will happily overflow one. The reverse
     layout's picture sits in the 1fr column, and between 900px and about
     964px of viewport that column is narrower than the cap. */
  .art-media .art-ink { max-width: min(100%, 414px); }
  .art-media .art-ink-lg {
    --plate-full: 621px;          /* 414 x 1.5 */
    --plate-scale: 1;
    max-width: min(100%, calc(var(--plate-full) * var(--plate-scale)));
  }
  /* A wide plate needs a wide column under it -- but ONLY in the
     picture-left layout, where the picture is in the fixed 420px column. In
     .reverse the picture is already in the 1fr column, which is the wider of
     the two (about 668px at the full wrap) and has the room for 621 without
     anything moving.

     Applying this to .reverse as well did the opposite of what it reads as:
     it grew the TEXT column, squeezed the picture's column to 458, and the
     drawing ran off the right-hand edge of the page. Both plates that use
     this are .reverse, so that is exactly what shipped to the screenshot. */
  .art.art-wide:not(.reverse) { grid-template-columns: 630px 1fr; }
}
.art-figure { margin: 0; }
.art-figure figcaption {
  margin-top: 12px; font-size: 13px; line-height: 1.5; color: var(--muted);
  max-width: 340px; margin-inline: auto;
}
.art-figure figcaption b { color: var(--text); }
/* The caption runs the full column width on a desktop. There used to be an
   .art-media .art-photo rule beside it, for the Matsuyama print standing in a
   drawing's slot; the print moved to the lineage page on 2026-10-02 and the
   rule had no users left.
   This has to sit below .art-figure figcaption, not above it -- a media
   query changes WHEN a rule applies, not how strongly, and the first attempt
   put it earlier in the file where the base rule simply won.
   check-css-order.py caught it. */
@media (min-width: 900px) {
  .art-figure figcaption { max-width: 100%; }
}
.art-ph {
  aspect-ratio: 4 / 3; background: var(--rule-soft);
  display: grid; place-items: center; border: 1px solid var(--rule);
}
.art-ph span { font-size: 54px; color: #a8a49b; line-height: 1; }
/* The heading and the two meta lines now live in .art-head, not .art-body.
   These were scoped to .art-body and silently stopped matching when the
   markup split -- the .art prefix keeps them working in either. */
.art h2 { font-size: clamp(26px, 3.6vw, 38px); margin: 0 0 10px; }
.art-body p, .art-head p { max-width: 62ch; margin: 0 0 1.05em; }
/* The style name, under the heading. The descriptor ("Classical Jujitsu")
   is what people search for and what tells them we are not a BJJ gym; the
   ryu is the precise answer for anyone who knows to ask. */
.art-ryu {
  font-family: var(--serif);
  font-style: italic;
  font-size: 17px;
  color: var(--red);
  margin: -4px 0 12px !important;
}

.art-when {
  font-size: 12px; font-weight: 700; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--muted); margin: 0 0 18px !important;
}
.art-lineage a {
  font-family: var(--serif); font-style: italic; font-size: 15px;
  color: var(--red); text-decoration: none;
}
.art-lineage a:hover { text-decoration: underline; }

/* A class a visitor cannot attend. For the reader -- distinct from
   .note-inline below, which is a note to the dojo. */
.restricted {
  background: var(--red-wash); border-left: 3px solid var(--red);
  padding: 12px 16px; font-size: 13.5px; max-width: 62ch;
}
.restricted b { color: var(--red); }

/* A note to the dojo, not to a visitor. Deliberately visible: this is a
   demo build, and an unanswered question should look unanswered. */
.note-inline {
  background: #fff6d6; border-left: 3px solid #e3cf8a; color: #5c4a11;
  padding: 12px 16px; font-size: 13px; max-width: 62ch;
}
.section-dark .note-inline { background: #2a2617; border-left-color: #6b5a22; color: #e6d9a8; }

/* Lineage chain: one master per step, with a rule running through. */
.lineage-chain { list-style: none; margin: 0; padding: 0; max-width: 62ch; }
.lineage-chain li { display: flex; gap: 20px; padding: 0 0 30px; position: relative; }
.lineage-chain li::before {
  content: ""; position: absolute; left: 8px; top: 20px; bottom: 0;
  width: 2px; background: var(--rule);
}
.lineage-chain li:last-child::before { display: none; }
.chain-mark {
  width: 18px; height: 18px; flex: none; border-radius: 50%;
  border: 3px solid var(--red); background: var(--paper-warm); margin-top: 3px; z-index: 1;
}
.lineage-chain li.current .chain-mark { background: var(--red); }
.lineage-chain b { display: block; font-family: var(--serif); font-size: 20px; font-weight: 700; }
.lineage-chain i {
  display: block; font-style: normal; font-size: 11px; font-weight: 700;
  letter-spacing: 0.11em; text-transform: uppercase; color: var(--red); margin: 3px 0 8px;
}
.lineage-chain p { margin: 0; color: var(--muted); font-size: 14.5px; }

/* Life dates, where they are known at all. Most of these people have none
   on record, so the line simply is not there rather than showing a guess. */
.masters .years {
  display: block; font-family: var(--serif); font-size: 13px;
  color: var(--muted); margin-top: 1px;
}

/* The drawing IS the entry for anyone Goody drew. A person can hold several
   pages -- Kent Hoffman has four -- so these stack inside the panel and the
   panel scrolls. */
.bio-plate { margin: 0 0 22px; }
.bio-plate a {
  display: block; background: var(--paper-warm);
  border: 1px solid var(--rule); padding: 8px;
  transition: border-color .15s ease;
}
.bio-plate a:hover { border-color: var(--red); }
.bio-plate img { width: 100%; height: auto; display: block; }
.bio-plate figcaption {
  margin-top: 10px; padding-left: 13px; border-left: 3px solid var(--red);
  font-size: 13.5px; line-height: 1.55; color: var(--text);
}
.plate-no {
  display: block; font-size: 10.5px; font-weight: 700; letter-spacing: 0.11em;
  text-transform: uppercase; color: var(--red); margin-bottom: 5px;
}
.bio-src-note {
  font-size: 12.5px !important; color: var(--muted); font-style: italic;
  margin: 0 0 4px !important;
}

/* Used by the text-only entries, kept for anything without a plate. */
.goody-caption {
  margin: 4px 0 0; padding: 14px 16px; background: var(--paper-warm);
  border-left: 3px solid var(--red); font-size: 14px; line-height: 1.55;
}
.goody-caption cite {
  display: block; margin-top: 9px; font-size: 11px; font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted);
  font-style: normal;
}

.masters { list-style: none; margin: 0 0 22px; padding: 0;
           display: grid; gap: 14px; grid-template-columns: 1fr; }
@media (min-width: 620px)  { .masters { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 980px)  { .masters { grid-template-columns: repeat(3, 1fr); } }
/* Group heading inside Those We Learned From -- The Lineage, Teachers of
   This School, Also Named. Quiet on purpose: it is a divider, not a section
   title, and the section already has one. */
.masters-head {
  font-family: var(--serif); font-size: 13px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--muted);
  margin: 0 0 12px; padding-bottom: 6px; border-bottom: 1px solid var(--rule);
}
.masters-head + .masters { margin-bottom: 30px; }
.masters li { background: var(--card); border-left: 3px solid var(--red); }
.masters a {
  display: block; padding: 16px 18px; text-decoration: none; height: 100%;
  transition: background .15s ease;
}
.masters a:hover, .masters a:focus-visible { background: var(--paper-warm); }
.masters b { display: block; font-family: var(--serif); font-size: 17px; }
.masters i { display: block; font-style: normal; font-size: 11px; font-weight: 700;
             letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
.masters .more {
  display: inline-block; margin-top: 9px; font-size: 11px; font-weight: 700;
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--red);
}

/* ---------------- biography panels ----------------
   CSS-only, because this site has no JavaScript. A name links to #bio-x;
   :target reveals that panel; the scrim and the x link to #closed, which
   matches no panel, so every .bio falls back to display: none.

   #closed is a zero-size, position: fixed element. That matters: the browser
   scrolls a fragment target into view, and a fixed element is always in view
   already, so there is nothing to scroll. Pointing the close links at
   #masters instead -- the obvious choice -- shut the panel but yanked the
   section heading to the top of the viewport.

   Honest limitation: :target cannot trap focus inside the panel the way a
   scripted dialog would. The panel is reachable and dismissable by keyboard,
   but a screen reader can still tab past it into the page behind. Worth
   knowing before this is called fully accessible. */
.close-anchor {
  position: fixed; top: 0; left: 0; width: 0; height: 0;
  overflow: hidden;
}
.bio { display: none; }
.bio:target {
  display: grid; place-items: center;
  position: fixed; inset: 0; z-index: 60; padding: 20px;
}
.bio-scrim { position: absolute; inset: 0; background: rgba(18, 18, 20, .72); }
.bio-card {
  position: relative; background: var(--card); color: var(--text);
  max-width: 580px; width: 100%; max-height: 84vh; overflow-y: auto;
  padding: 34px 34px 28px; border-top: 4px solid var(--red);
  box-shadow: 0 24px 60px rgba(0,0,0,.4);
}
.bio-x {
  position: absolute; top: 10px; right: 16px; font-size: 26px; line-height: 1;
  text-decoration: none; color: var(--muted);
}
.bio-x:hover { color: var(--red); }
.bio-card h3 { font-family: var(--serif); font-size: 27px; font-weight: 700; margin: 0 0 4px; }
.bio-meta {
  font-size: 11px; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--red); margin: 0 0 18px;
}
.bio-card p { font-size: 15px; margin: 0 0 1em; max-width: none; }
.bio-src {
  border-top: 1px solid var(--rule-soft); padding-top: 14px; margin: 18px 0 0 !important;
  font-size: 12.5px !important; color: var(--muted); font-style: italic;
}

/* Instructor portraits. The dojo's own photographs, and two of the three
   are only 200px on the original, so they are shown small and sharp rather
   than stretched. */
.portrait {
  width: 108px; height: 108px; border-radius: 50%;
  object-fit: cover;
  /* Centred in the card on Astone's word, 2026-09-24. `img` is display:
     block site-wide, so without the auto margins these sit hard against the
     card's left padding. */
  margin: 0 auto 16px;
  border: 3px solid var(--rule-soft);
}

.map-credit {
  font-size: 11px; color: var(--muted); margin: 8px 0 0; text-align: right;
}

.rates { list-style: none; margin: 0 0 22px; padding: 0; max-width: 60ch; }
.rates li { display: flex; gap: 20px; align-items: baseline;
            padding: 15px 0; border-bottom: 1px solid var(--rule); }
.rates li:last-child { border-bottom: 0; }
.rates b { font-family: var(--serif); font-size: 27px; color: var(--red);
           min-width: 92px; flex: none; }
.rates span { color: var(--muted); }

/* SENSEI STEVE BESIDE THE DUES -- astone, 2026-10-02.

   The rates were a single stacked column. They are now the left of a two
   column grid with the drawing on the right, which is the only structural
   change: the list, the fineprint and the button are untouched inside it.

   `align-items: center` rather than start, because the figure is taller than
   the list and anchoring him to the top left a long tail of empty paper
   under the prices.

   On a phone it collapses to one column and he comes LAST -- somebody on a
   phone opened this section for the numbers, and a full-height drawing above
   them would push every price below the fold. */
.rates-split { display: grid; gap: 30px; grid-template-columns: 1fr; }
.rates-art {
  display: block; width: auto; height: auto;
  max-width: min(100%, 300px); margin: 0 auto;
  mix-blend-mode: multiply;
}
@media (min-width: 900px) {
  .rates-split { grid-template-columns: 1fr 320px; column-gap: 44px;
                 align-items: center; }
  .rates-art { max-width: min(100%, 320px); }
}

/* ---------------- documents ---------------- */

/* A reproduced document. The frame and mount are deliberate: these are
   photographs of paper, and showing them as plates rather than as page
   furniture keeps that legible. */
.plate { margin: 0; background: var(--card); padding: 18px; border: 1px solid var(--rule); }
.plate img { width: 100%; height: auto; display: block; }
.plate figcaption {
  margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--rule-soft);
  font-size: 13.5px; color: var(--muted); max-width: 70ch;
}
.plate figcaption b { color: var(--text); font-weight: 700; }
.plate figcaption a { color: var(--red); text-decoration: none; white-space: nowrap; }
.plate figcaption a:hover { text-decoration: underline; }

/* Plates in a row are different shapes -- the strikes chart is short and
   wide, the throws chart tall. Laying each one out as a column with the
   image area growing centres every image on the row's midline and lines the
   captions up along the bottom, instead of short plates floating at the top. */
.plate { display: flex; flex-direction: column; }
.plate figcaption { margin-top: auto; }

/* Eighteen scanned pages. Smaller cells than the charts -- these are read by
   opening them, so the grid only has to be browsable. */
.pages-grid {
  display: grid; gap: 16px;
  grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 620px)  { .pages-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 900px)  { .pages-grid { grid-template-columns: repeat(4, 1fr); } }
@media (min-width: 1100px) { .pages-grid { grid-template-columns: repeat(6, 1fr); } }

.page { margin: 0; display: flex; flex-direction: column; }
.page > a {
  display: block; background: var(--card); border: 1px solid var(--rule);
  padding: 7px; position: relative; transition: border-color .15s ease;
}
.page > a:hover, .page > a:focus-visible { border-color: var(--red); }
.page img { width: 100%; height: auto; display: block; }
.page figcaption {
  font-size: 11.5px; color: var(--muted); margin-top: 7px; line-height: 1.35;
}
.page figcaption b { color: var(--red); }

.chart-grid { display: grid; gap: 22px; grid-template-columns: 1fr; }
@media (min-width: 820px) {
  .chart-grid { grid-template-columns: 1fr 1fr; }
  .chart-grid .plate.wide { grid-column: 1 / -1; }
}
.chart-grid .plate img {
  max-height: 620px; max-width: 100%; width: auto; margin: 0 auto;
}
/* width:100% here stretched a 736px-wide original across a 1100px column
   and made it blurry. These scans are small; never render one above its
   natural size. */
.chart-grid .plate.wide img { max-height: none; width: auto; max-width: 100%; }

/* The Chen genealogy on /history/ is 2900px of columns. Inside a .prose
   block it is capped at 62ch and reads as a grey smudge, so it sits at full
   wrap width between two .prose blocks instead. Same rule as the charts
   above about never rendering above natural size -- the thumbnail is 1000px
   and the wrap is wider than that. */
.plate-chart { margin: 26px auto 30px; }

/* A single photograph opening a section, outside the .prose column so the
   62ch measure does not shrink it, but capped well short of the full wrap --
   at wrap width a near-square print swamps the heading above it. */
.plate-portrait { max-width: 720px; margin: 0 auto 34px; }
/* The credit line inside a plate caption: smaller, muted, on its own line,
   because it is a condition attached to the image rather than something the
   reader needs in the sentence. */
.plate figcaption .fineprint { display: block; margin: 8px 0 6px; font-size: 12px; }
/* Inside a .section-head it is a grid item between the kicker and the
   heading, so the grid's own gap does the spacing -- its bottom margin would
   stack on top of that. */
.section-head .plate-portrait { margin: 4px auto 6px; }
/* A cut-out emblem, not a photograph of a document -- so no card, no border
   and no padding. It is a transparent PNG and the paper should show through
   it. */
.crest { margin: 6px auto 10px; max-width: 340px; text-align: center; }
.crest img { width: 100%; height: auto; }
.crest figcaption {
  margin-top: 12px; font-size: 13px; color: var(--muted);
  text-align: left; max-width: 46ch; margin-inline: auto;
}
.plate-chart img { width: auto; max-width: 100%; margin: 0 auto; }

/* Every plate opens at full size. The grid shrinks these to fit, and some
   of the captions are unreadable until the file is opened, so the affordance
   has to be visible rather than discovered. */
.plate a { text-decoration: none; }
/* One rule for the image area, and it must come after `.plate a` -- an
   earlier version set display:flex here and display:block there, same
   specificity, so the later rule won and every short image sat at the top of
   its cell instead of centred against a taller neighbour. */
.plate > a {
  flex: 1;
  display: flex; align-items: center; justify-content: center;
  position: relative; background: var(--paper-warm);
  outline-offset: 3px; transition: opacity .15s ease;
}
.plate > a::after {
  content: "Open full size";
  position: absolute; left: 50%; bottom: 10px; transform: translateX(-50%);
  background: rgba(18,18,20,.82); color: #fff;
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; padding: 6px 12px;
  opacity: 0; transition: opacity .15s ease; pointer-events: none;
}
.plate > a:hover::after, .plate > a:focus-visible::after { opacity: 1; }
.plate > a:hover img { opacity: .92; }
@media (hover: none) {
  /* no hover on a touch screen, so state it permanently */
  .plate > a::after { opacity: 1; }
}

.gloss-key { font-size: 13px; color: var(--muted); max-width: 70ch; margin: 0 0 26px; }
/* Terms sourced from outside the dojo's own Definitions.txt used to carry a
   red bullet here, with a legend above the glossary. Removed 2026-09-18:
   nobody connected the dot to the legend -- the person who commissioned the
   page asked what the red dots meant -- and red reads as "error" rather than
   "sourced elsewhere". build-glossary.py still marks those entries with
   class="added", and the provenance is recorded against every one of them in
   site/data/glossary-extra.json, which is where it is actually useful. */

/* Japanese sits next to the romaji, at a slightly larger size -- kanji
   render small for their point size against Latin text. */
.ja {
  font-size: 1.15em; margin-left: 7px; color: var(--muted);
  font-weight: 400; letter-spacing: 0;
}

.gloss-grid { column-count: 1; column-gap: 34px; }
@media (min-width: 720px)  { .gloss-grid { column-count: 2; } }
@media (min-width: 1040px) { .gloss-grid { column-count: 3; } }

/* break-inside keeps a heading from being stranded at the foot of a column */
.gloss-block { break-inside: avoid; margin: 0 0 30px; }
.gloss-block h3 {
  font-family: var(--serif); font-size: 17px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
  margin: 0 0 12px; padding-bottom: 7px; border-bottom: 2px solid var(--red);
}
.gloss { margin: 0; }
.gloss div { padding: 4px 0; border-bottom: 1px solid var(--rule-soft); }
.gloss dt { font-weight: 700; font-size: 13.5px; margin: 0; }
.gloss dd { margin: 1px 0 0; font-size: 13px; color: var(--muted); }
.gloss-list { margin: 0; padding-left: 18px; font-size: 13.5px; }

/* A note inside the glossary's multi-column layout. Without break-inside it
   can split across a column boundary mid-sentence, and the 62ch measure it
   inherits from .note-inline is wider than a column. */
.gloss-note { break-inside: avoid; max-width: none; margin: 0 0 22px; }
.gloss-list li { padding: 3px 0; }

.center-block { text-align: center; }
.center-block .lede { margin: 0 auto 24px; }
.center-block h2 { margin-bottom: 14px; }
.center-cta { text-align: center; margin: 38px 0 0; }

.section-dark .prose p, .section-dark .rates span { color: #c9c9d0; }
.section-dark a { color: #ff8a7f; }

/* ---------------- font specimen (/specimen.html) ----------------
   Lifted out of that page: the CSP is style-src 'self', which refuses an
   inline <style> block, so the page rendered unstyled the moment it was
   served with real headers. Every selector is scoped to .specimen so a
   tool page cannot reach into the site. */
.specimen /* Specimen-only styles. This page is a tool, .specimen not part of the site. */
  .row { border-bottom: 1px solid var(--rule); padding: 1.4rem 0; }
.specimen .row:last-child { border-bottom: 0; }
.specimen .lbl { font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase;
         color: var(--ink-soft); margin: 0 0 0.5rem; max-width: none; }
.specimen .sample { font-family: var(--font-display); letter-spacing: 0.02em;
            line-height: 1.15; margin: 0; max-width: none; }
.specimen .body-sample { max-width: var(--measure); margin: 0; }
.specimen .cols { display: grid; gap: 1.5rem; grid-template-columns: 1fr; }

  @media (min-width: 46rem) { .cols { grid-template-columns: 1fr 1fr; }
.specimen .card { background: var(--paper-alt); padding: 1.25rem; }
.specimen .card p { max-width: none; }
.specimen .bad { border-left: 3px solid var(--accent); padding-left: 1rem; }
.specimen .glyphs { font-family: var(--font-display); font-size: 1.6rem; line-height: 2;
            word-break: break-all; max-width: none; }

/* size demonstrations, replacing inline style attributes the CSP refuses */
.specimen .fs-18 { font-size: 18px; }
.specimen .fs-22 { font-size: 22px; }
.specimen .fs-30 { font-size: 30px; }
.specimen .fs-34 { font-size: 34px; }
.specimen .fs-40 { font-size: 40px; }
.specimen .fs-56 { font-size: 56px; }
.specimen .in-brand { font-family: var(--font-display); }

/* .wordmark, .tagline and .note were defined in the pre-Stitch stylesheet
   and deleted with it; the specimen page still used them. */
.specimen .wordmark { font-family: var(--brand); font-size: clamp(2rem, 6vw, 3.5rem);
                      letter-spacing: 0.03em; line-height: 1.1; color: var(--text);
                      text-decoration: none; display: inline-block; }
.specimen .tagline { color: var(--muted); margin: 0.35rem 0 0; font-size: 1.05rem; }
.specimen .note { background: var(--paper-warm); border-left: 3px solid var(--red);
                  padding: 1rem 1.25rem; margin: 2rem 0; max-width: 62ch; }
.specimen .note p:last-child { margin-bottom: 0; }

/* ===================================================================
   THE HANGING SCROLL -- home page only, desktop only.

   Astone's mockup, 2026-10-07: the page content hangs from a wooden rod as a
   kakemono, deckled down both sides, against an ink-wash wall. One page, and
   only above 1360px. Phones and narrow laptops get the site exactly as it
   was, by decision rather than by omission.

   1400px is where the arithmetic works rather than a round number, and it
   went up from 1360 when the roll was stretched to the full panel: the
   finials add about 13.6% to the panel's width, and at 1360 that left under
   2px of wall beside them once a scrollbar was counted. The panel is capped
   at 1320 and floored at .wrap's own 1180, or the decoration starts squeezing
   the content column, which is the wrong way round. At 1400 there is 21px of
   wall either side of the finials; at 1920 there is 200px, which is where the
   effect is meant to land.

   A 1366-wide laptop therefore gets the plain page. That is the honest line:
   below 1400 the scroll does not fit without the handles touching the edge of
   the screen.

   Scoped with :has() on .scroll-page, a marker only this page carries.
   The masthead and <main> live in the shared layout, so this is what lets one
   page change them without teaching build.py about body classes.

   The furniture is built by scripts/build-scroll.py.
   =================================================================== */
.scroll-frame { position: relative; }
.scroll-page, .scroll-rod, .scroll-chop { display: none; }

@media (min-width: 1400px) {
  /* EVERY NUMBER BELOW IS MEASURED OFF THE PLATE, not chosen. The roll is
     2584x133; within it the white paper runs x 151..2429 and the roll's own
     foot is at y 123. Those three facts fix the whole arrangement, and if
     astone sends another plate they are the only things to re-measure:

       2584 / 2275  = 1.13582   plate width over the width of its white paper
       133  / 2584  = 0.051470  plate aspect
       120  / 133   = 0.90226   the HIGHEST point of the roll's sloping foot

     Re-measured 2026-10-07 after astone could still see a gap on the right
     and the paper still overhanging the white. Both came from measuring too
     loosely the first time: the paper span was read off short light runs at
     the extremes rather than the longest contiguous one, and the foot was
     taken at the roll's LOWEST point when the edge is not level at all. */
  :root {
    /* clamp, not min, and 200px of side room rather than 120. Widening the
       roll so its paper matches the sheet pushed the finials further out than
       the old margin reserved: at 1360-1490px of viewport the rod came out
       WIDER than the window and overflowed. The floor of 1180 is .wrap's own
       width -- the panel must never fall below it or the decoration starts
       squeezing the content column, which is the wrong way round.

       Checked across 1360..2560: the rod now clears the viewport everywhere,
       with 53px to spare at the tightest point. */
    --panel:     clamp(1180px, 100vw - 240px, 1320px);
    --tear:      36px;
    /* THE WHITE MATCHES THE WHOLE PANEL, not the sheet's average width.

       The torn boundary wanders: its mean sits 13.5px inside the panel edge
       but it swings about 7px either way, so sizing the roll to that mean --
       which the previous attempt did -- left the paper poking out past the
       white wherever the tear ran wide. Astone, 2026-10-07: "the scroll still
       isn't stretched enough". Sized to the full panel the white is never
       narrower than the sheet; at worst it is a few pixels proud, which is how
       a roll and the sheet wound onto it actually look. */
    --rod-w:     calc(var(--panel) * 1.13582);
    --rod-h:     calc(var(--rod-w) * 0.051470);
    /* THE ROLL'S FOOT IS NOT LEVEL -- measured, it runs y=124 at the far left
       and y=120 through the middle and right, a 4px lean across the plate. A
       straight band cannot meet a sloping edge everywhere, so it aligns to the
       HIGHEST point of it rather than the lowest. Overlap is invisible, since
       the roll is drawn over the sheet; a gap is not, and aligning to the
       lowest point is exactly what left wall showing on the right. */
    --roll-foot: calc(var(--rod-h) * 0.90226);
    --mast-h:    94px;                          /* .masthead .wrap min-height */
    /* Measured off the rendered footer at this width, 445px, plus slack so
       the sheet clears its top rather than stopping flush with it. */
    --foot-h:    470px;
    --chop:      62px;   /* 76 wall px at the 0.8157 the wall draws at */
    --rod-top:   calc(var(--mast-h) - 10px);    /* tucked under the masthead */
  }

  /* THE WALL IS BODY'S OWN BACKGROUND, and nothing on this page uses a
     negative z-index any more.

     IT USED TO BE A FIXED LAYER AT z-index: -1, and that broke on astone's
     work laptop (Brave, 2026-10-08): the margins showed paper-crumple and no
     wall at all, while the same image rendered perfectly well as the
     masthead's background. A negative-z element paints above the CANVAS
     background, and body's background only reaches the canvas while the root
     element has none of its own. Give <html> a background -- an extension, a
     user style, a force-dark pass -- and the propagation stops, body's paper
     becomes an ordinary element background painted later, and every
     negative-z layer vanishes behind it. Too fragile a rule to hang the whole
     backdrop on, so the dependency is gone rather than diagnosed remotely.

     `fixed` attachment is the one thing the body texture is on record as NOT
     doing, and the objection does not reach it: nothing slides over the wall,
     because the sheet is opaque and covers it. What is left either side is
     wall, and a wall is meant to stay put. Scrolling it is not on offer
     anyway -- one 1324px plate either runs out after a screen or repeats, and
     bamboo in one corner against mountains in the other repeats visibly.

     blend-mode back to normal: the base rule multiplies the paper tile into
     the paper colour, which would muddy a full-colour plate. */
  body:has(.scroll-page) {
    background-image: url('/img/scroll-backdrop.jpg');
    background-position: center center;
    background-size: cover;
    background-repeat: no-repeat;
    background-attachment: fixed;
    background-blend-mode: normal;
  }

  /* THE MASTHEAD AND THE ROD DO NOT MOVE. THE PAPER DOES.

     Astone, 2026-10-07: "the information being wound up into the scroll". The
     wordmark, the characters, the nav and the button are pinned; the sheet
     slides up behind them and is gone at the rod, the way a kakemono winds
     onto its own dowel.

     The masthead has to be OPAQUE for that, or the sheet shows through it on
     the way past. It carries the wall as its own background with
     `fixed` attachment, which makes the viewport the positioning area -- the
     same area body's own wall covers, at the same `center / cover`. So the
     two line up exactly and the band is invisible as a band.

     Its padding-bottom carries it down to precisely the foot of the rod, so
     there is no seam between the band and the sheet and no gap where the
     paper could reappear above the rod. That is why the padding is derived
     from --rod-h rather than typed in.

     border-bottom goes because it was drawing a hairline across the wall --
     astone, 2026-10-07: "a small white line under the title". It is still
     there on the other five pages, where it is a rule on paper and reads as
     one. */
  body:has(.scroll-page) .masthead {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 10;
    border-bottom: 0;
    padding-bottom: calc(var(--roll-foot) - 10px);
    background: url('/img/scroll-backdrop.jpg') center center / cover fixed;
  }

  /* Above the band, so it reads as lying on the wall rather than under it.
     Its own element rather than a pseudo on the sheet: a mask clips its
     element's pseudo-elements too, so a rod drawn on .scroll would have its
     finials torn off at the same ragged line as the paper. */
  .scroll-rod {
    display: block;
    position: fixed;
    z-index: 11;
    top: var(--rod-top);
    left: 50%;
    transform: translateX(-50%);
    width: var(--rod-w);
    height: var(--rod-h);
    pointer-events: none;
    background: url('/img/scroll-rod.png') center / 100% 100% no-repeat;
  }

  /* The masthead is out of flow now, so <main> has to carry the height back.
     6px short of the rod's foot, so the top of the sheet finishes behind the
     band instead of ending on a visible straight edge. */
  /* THE FOOTER IS PAINTED ON THE WALL AND THE PAPER UNCOVERS IT.

     Astone, 2026-10-07: it had no paper under it yet scrolled as though it
     did, which read as a mistake either way. So it is pinned to the foot of
     the window, and scrolling to the end lifts the paper off it.

     It sits at z-index 0 with <main> lifted to 1, rather than the footer at
     -1. Same order, but nothing negative: the wall is body's own background
     now, underneath both, and no layer on this page depends on painting above
     the canvas. See the note on body:has(.scroll-page) for what that cost.

     It does not need hiding while the sheet covers it: the footer's box is
     full width but everything in it lives in a .wrap no wider than 1180, so
     what you can actually see of it is narrower than the sheet.

     border-top goes for the same reason the masthead's did -- on paper it is
     a rule, on the wall it was a white hairline, and it was the line astone
     could see at the end of the paper. The one inside .foot-bottom stays,
     because it is still dividing two things, but it goes dark: a light rule
     on sepia reads as a scratch. */
  body:has(.scroll-page) .sitefoot {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 0;
    border-top: 0;
  }
  body:has(.scroll-page) .foot-bottom { border-top-color: rgb(0 0 0 / .14); }

  /* ASTONE'S CHOP, revealed at the end of the page.

     MEASURED FROM THE VIEWPORT BOTTOM, not placed on the wall. Painting it
     into the wall plate was the obvious thing and it cannot work: the wall
     is `cover`, so it scales with the window, while the strip the paper
     uncovers is the window's foot less a constant. Solve for the wall rows
     that are both below the sheet's foot and above the Dojo Dispatch
     heading and the answer slides -- 538 to 573 at a 900px viewport, 748 to
     781 at 1080, 880 to 888 at 1400, and no row is shared by any two. On the
     wall it ends up buried under the paper or sitting on the footer
     depending on the window; astone could not see it at all.

     Anchored to the bottom it tracks the reveal by construction. The sheet's
     foot at full scroll is --foot-h above the window's foot and the heading
     is about 89px below that, so the seal's top has to clear --foot-h and
     its bottom stay above --foot-h less 89. These two numbers give 16px of
     air under the paper's edge and 11px over the heading, at any height.

     THAT CLEARANCE IS NOW SPENT. Astone asked for 20px up on 2026-10-08 and
     there were only 16, so the seal's top sits 4px ABOVE the sheet's foot and
     tucks under the torn edge at full scroll. Checked at exactly the document
     height, which is the only render that shows the true relationship: it
     reads as the seal sitting just under the paper rather than as a clip,
     because the tear is ragged and bites further up than that anyway. Any
     further up and it goes behind the paper -- raise --foot-h to buy room.

     z-index 0 puts it with the footer: above the wall, under the sheet. */
  .scroll-chop {
    display: block;
    position: fixed;
    z-index: 0;
    width: var(--chop);
    height: var(--chop);
    /* Hard right, but anchored to the PANEL rather than to the centre, so it
       stays inside the sheet's band at every width -- outside it the paper
       never covers it and there is no reveal left to have.

       110px in from the panel's edge -- astone, 2026-10-08: 100px left of
       where it was, which had it overlapping the mountain. It now lands just
       right of the Dojo Dispatch heading and clear of the drawing. */
    right: calc(50% - var(--panel) / 2 + 110px);
    bottom: calc(var(--foot-h) - var(--chop) + 4px);
    pointer-events: none;
    background: url('/img/chop-seal.png') no-repeat center / contain;
  }

  /* The masthead is out of flow now, so <main> has to carry the height back,
     and the footer is too, so <main> has to carry the room to reveal it.

     pointer-events is the part that is easy to miss: that bottom padding is
     still part of <main>'s box, and a box with no background still swallows
     clicks. Without this the revealed footer would look right and be dead --
     the phone number, the email and the nav all unclickable. The sheet turns
     them back on for everything that is actually in it. */
  body:has(.scroll-page) #main {
    padding-top: calc(var(--rod-top) + var(--roll-foot) - 8px);
    padding-bottom: var(--foot-h);
    pointer-events: none;
  }
  /* .bio is in this list because the biography panels live INSIDE <main> but
     OUTSIDE .scroll -- they had to leave the sheet so the mask would stop
     making itself their containing block. That put them in the one place
     that gets pointer-events: none and nothing to turn it back on, so they
     rendered perfectly and were completely dead: the card would not scroll
     and the close link would not take a click. Astone found it on
     /history/, 2026-10-09.

     Anything else moved out of the sheet but left in <main> needs adding
     here too. The rod, the seal and the marker do not: they are decoration
     and are meant to be transparent to the pointer. */
  body:has(.scroll-page) .scroll,
  body:has(.scroll-page) .bio { pointer-events: auto; }

  /* THE FOOT IS TORN OUT HERE, ONE LEVEL OUT FROM THE SHEET.

     The first attempt did it inside .scroll's own mask and was wrong twice
     over, exactly as astone saw: the bottom band ran the full panel width
     while the sides are torn ~13px inwards, so the last 26px of the sheet
     was visibly WIDER than the rest of it; and cutting the sheet square and
     adding the band back left a hairline at the join you could see through.

     Out here neither can happen, because A MASK ONLY REVEALS PAINT -- it can
     never add any. By the time this applies, .scroll has already thrown away
     everything outside its torn sides, so a full-width band at the foot
     cannot widen anything; it reveals paper where there is paper and wall
     where the side tear already removed it. The two tears meet at the corners
     by intersection, which is what they should have done all along.

     The gradient overlaps the band by 1px. Butted exactly they can leave a
     sub-pixel sliver on a fractional element height, which is the line that
     was showing. */
  .scroll-frame {
    max-width: var(--panel);
    margin: 0 auto;
    /* Positioned so it outranks the fixed footer at z-index 0 and the paper
       covers it until the end of the page.

       THE Z-INDEX IS HERE AND NOT ON #main, and that is the whole point. Put
       on <main> it makes <main> a stacking context, which traps the rod
       inside it: the rod asks for z-index 11 to sit above the masthead's 10,
       but inside a context of 1 it can never beat anything outside it, so the
       masthead's opaque band paints straight over the rod and the scroll
       loses its roll entirely. On .scroll-frame, <main> stays transparent to
       stacking, the rod still reaches the root context, and only the sheet
       is lifted -- which is all that needed lifting. */
    position: relative;
    z-index: 1;
    /* THE FOOT IS MASKED HERE, on the frame, not on ::before. It spent a
       while on the pseudo-element by accident and the result was a dead
       straight bottom edge -- it was trimming the rim overlay, which has
       nothing at the foot to trim, and never touching the sheet. */
    -webkit-mask-image: linear-gradient(#000, #000),
                        url('/img/scroll-edge-bottom.png');
    -webkit-mask-repeat: no-repeat, no-repeat;
    -webkit-mask-position: left top, left bottom;
    -webkit-mask-size: 100% calc(100% - var(--tear) + 1px), 100% var(--tear);
    mask-image: linear-gradient(#000, #000),
                url('/img/scroll-edge-bottom.png');
    mask-repeat: no-repeat, no-repeat;
    mask-position: left top, left bottom;
    mask-size: 100% calc(100% - var(--tear) + 1px), 100% var(--tear);
  }

  /* THE FIBRE LIP, which the mask cannot do -- a mask only removes pixels, so
     a lighter edge has to be PAINTED. These are the torn paper's own pixels
     from astone's plate, in a band running inward from the cut and fading
     into the sheet, so against the dark hero it reads as fibre peeled back.
     Over the pale sections it disappears, which is right: a torn edge only
     catches the light against something darker.

     Cut from the same plate at the same coordinates as the mask, so the lip
     and the cut cannot drift out of register.

     It sits on .scroll-frame rather than inside .scroll because .scroll's own
     mask would clip it to the paper, and the lip has to sit ON the tear. */
  /* THE SEAM, and the parallax. Astone, 2026-10-08.

     Insetting the hero so the tear had paper to sit on left it a dark
     rectangle with hard vertical sides -- a photograph pasted on the sheet.
     Feathered over 30px it sinks into the paper instead. Measured first: at
     the widest panel the hero's text starts 50px in from its own edge, so a
     30px feather cannot touch it.

     background-attachment: fixed is the parallax. The photograph is pinned
     to the viewport while its own section scrolls over it, so it drifts
     against the page instead of travelling with it. This is the same
     property the note on `body` rejects for the paper texture, and for the
     same reason it is right here and wrong there: on `body` the words slide
     over a dead-still texture, whereas here the thing that stays put is a
     PHOTOGRAPH being looked at through a moving window, which is what
     parallax is. */
  /* NO background-attachment here. It was tried on 2026-10-08 as "parallax"
     and it was a misread: pinning the photograph to the viewport takes it
     off the sheet entirely -- it stops scrolling with the paper and the
     fighters end up out of frame, because `fixed` sizes to the viewport and
     not to the hero's box. The hero travels with the sheet. */
  /* .section-dark covers the page heads -- which carry that class -- and the
     dark closing bands. The home page has neither, so this only started
     mattering when /disciplines/ went on the scroll: a full-bleed dark band
     inset on the sheet has hard vertical sides and reads as a photograph
     pasted on, exactly as the hero did before it was feathered.

     Safe at 30px on all of them: every one puts its text in a .wrap, which
     at the widest panel starts 50px in from the band's own edge. */
  body:has(.scroll-page) .hero,
  body:has(.scroll-page) .section-dark {
    -webkit-mask-image: linear-gradient(to right, transparent 0,
                        #000 30px, #000 calc(100% - 30px), transparent 100%);
    mask-image: linear-gradient(to right, transparent 0,
                #000 30px, #000 calc(100% - 30px), transparent 100%);
  }

  /* ONE PAPER, NOT TWO. .scroll carries the texture now, so .paper-crease
     inside it must stop carrying its own.

     Astone, 2026-10-08: "shifts where they meet and color changes where some
     paper squares have a more yellow color". That is exactly what two
     multiplying layers of the same tile do -- the texture lands twice
     wherever .paper-crease covers, which darkens and warms it into a visible
     rectangle, and the two elements start their 820px grid at different
     origins, so the tiles are out of phase at the boundary.

     This is the same argument the .paper-crease comment already makes for
     using one wrapper per page instead of a background per section. The
     wrapper just moved up a level on this page: .scroll is now the single
     element the tile runs down, continuously, the whole height of the
     sheet. */
  body:has(.scroll-page) .paper-crease {
    background-color: transparent;
    background-image: none;
  }

  .scroll-frame::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 2;
    pointer-events: none;
    background:
      url('/img/scroll-rim-left.png')  repeat-y left  top / var(--tear) auto,
      url('/img/scroll-rim-right.png') repeat-y right top / var(--tear) auto;
  }

  /* A fixed header eats the top of whatever an in-page link jumps to, because
     the browser scrolls the target to y=0 and the band is sitting on it. The
     home page has four of these -- #why and the section ids the nav and the
     footer point at. scroll-margin-top moves the landing point down by the
     exact height of the furniture, plus a little air. */
  body:has(.scroll-page) [id] {
    scroll-margin-top: calc(var(--rod-top) + var(--roll-foot) + 16px);
  }

  /* Three mask layers: the two torn strips at the edges, and a flat gradient
     filling everything between them. Layers union by default, so the strips
     and the middle become one sheet-shaped mask -- no mask-composite, and
     nothing to go wrong where it is unsupported.

     The strips are authored at 56px and drawn at 26, so they land at better
     than 2x. They repeat down the page and are built to tile: every wave in
     the tear is a whole number of cycles over the strip, so there is no seam
     to find.

     THE FOOT IS NOT MASKED HERE. It is on .scroll-frame, one level out --
     see the note there for why it cannot be done in this rule. */
  /* THE TEST, 2026-10-08: give the tear paper to belong to.

     In astone's reference the lip is continuous with the sheet's own paper
     and the dark photograph sits INSET from the torn edge. Here the hero was
     full-bleed and ran straight to the tear, so a pale lip lay directly on a
     dark photograph with nothing of its own behind it -- which reads as a
     rope on the picture, and is what he saw three times running. No amount
     of better fibre addresses that; it is the layout.

     So the sheet carries the paper, and its contents start clear of the tear.
     The padding is wider than --tear so the whole torn band falls on paper. */
  .scroll {
    background-color: var(--paper-warm);
    background-image: url('/img/paper-crease.jpg');
    background-size: 820px 820px;
    background-repeat: repeat;
    background-blend-mode: multiply;
    padding: 0 44px;
    -webkit-mask-image:
      url('/img/scroll-edge-left.png'),
      url('/img/scroll-edge-right.png'),
      linear-gradient(#000, #000);
    -webkit-mask-repeat: repeat-y, repeat-y, no-repeat;
    -webkit-mask-position: left top, right top, var(--tear) top;
    -webkit-mask-size: var(--tear) auto, var(--tear) auto,
                calc(100% - var(--tear) * 2) 100%;
    mask-image:
      url('/img/scroll-edge-left.png'),
      url('/img/scroll-edge-right.png'),
      linear-gradient(#000, #000);
    mask-repeat: repeat-y, repeat-y, no-repeat;
    mask-position: left top, right top, var(--tear) top;
    mask-size: var(--tear) auto, var(--tear) auto,
                calc(100% - var(--tear) * 2) 100%;
  }
}
