/* ---------------------------------------------------------------------------
   site/styles — Dieter Rams designs a liturgical book
   ---------------------------------------------------------------------------
   Blacks, greys, one liturgical red. Nothing decorative that is not also
   informative.

   THE SYSTEM ANSWERS FOUR QUESTIONS. Everything below is a token or a rule,
   and each belongs to one of them.

   ─── 1 · WHAT COLOUR IS THIS? ────────────────────────────────────────────
   Six values, and three of them name a JOB rather than a shade.

   --ink --rubrica --heading --label carry text. (--ghost does NOT: it is a
   mark, and the note on the token says why.)

   --heading sits between --ink and --label, and the gap it fills is a real
   one: a chart title, a table caption and the label over a control NAME the
   thing beneath them, where --label is the voice of metadata and glosses. A
   heading set in the aside's colour reads as commentary on its own contents.

   --rule SEPARATES and --edge AFFORDS. A rule is a hairline in a book: it
   divides a row from the next, and 1.37:1 against paper is right for it,
   because nothing asks to touch it. An edge is the boundary of a control and
   has to be seen — 3.40:1, the floor WCAG sets for a non-text boundary. One
   token doing both work gave every field on the page a separator where it
   needed an edge.

   THE PAPER IS ONE COLOUR. --paper-lit is declared and currently unspent: a
   selected row is marked by its rubrica bar alone. Lit paper under a row read
   as a panel rather than a lift — in a table it sat between two hairlines,
   and in the chant list it made a band across a column that has no other
   fills. Nothing changes a background: a figure is given weight by its own
   structure — the strokes it is drawn with — not by a panel of tint.

   ─── 2 · HOW BIG, AND HOW LOOSE? ─────────────────────────────────────────
   Six type steps, --micro 9 through --display-size 26. They are the same six
   the figures draw with (STEP in diagrams/ink.js), so a wheel's label and a
   table's label at the same step are the same size. The wordmark is off the
   scale deliberately and is named --wordmark-size, not a seventh step.

   TRACKING IS OPTICAL, so it falls as size rises. Three values:
   --track-micro  .09em  capitals at 9px — headings, table heads, labels
   --track-caps   .05em  capitals at 11px and up
   --track-meta   .04em  mono metadata, upright or italic
   Serif names take none.

   THREE FACES, EACH WITH A JOB.
   Junicode carries words and Latin. IBM Plex Mono carries machine data — ids,
   hz, cents, ratios. Jacquard is the wordmark, once. That rule decides every
   label: a month name, a sign name, a gamut role and a chironomy letter are
   all WORDS, so all of them are serif, wherever they are drawn. There is no
   sans, because a fourth face would have no job and no file behind it.

   ─── 3 · HOW MUCH AIR? ───────────────────────────────────────────────────
   --sp-1 … --sp-9, on a 4px base. A step is a choice from nine.

   Three things are deliberately NOT on the scale, each said so where it
   sits: --page-x (the margin of the leaf, which changes with the window),
   the chant row's `calc(40% - 14px)` (load-bearing for where the staff
   starts — re-measure before touching), and the temperament slider (hand
   tuned against the running control).

   ─── 4 · WHAT IS THIS THING, AND WHAT STATE IS IT IN? ────────────────────
   THE COSTUME RULES. These are what tell a filter from a jump.

   1. A BOX means a value lives here.          a field, a select, a range
   2. An UNDERLINE means this is where you are. tabs, the view switch
   3. Neither means this does something.        a spur, a segment of a set
   4. A SET IS ONE OBJECT MADE OF PARTS.        adjacent segments, one edge,
      a tick each — the offices, the analysis tracks, aspectus.

   STATE IS TWO MOVES AND NO WASH.
   HOVER DARKENS — the row's rule goes --rule to --edge, and its name comes
   up to full ink.
   SELECTION IS MARKED, NOT FILLED — a 2px rubrica bar in the margin, and
   nothing else. In a table the bar rides the first cell as an inset shadow,
   so it takes no width and cannot shift the column.

   FOCUS IS ONE SENTENCE: 2px rubrica, OUTSIDE a box that carries its own
   edge, INSIDE anything full-bleed — a row, a tab — where an outside ring
   would be clipped or collide with the row above. There is no third case.

   ─── WHAT KEEPS IT HONEST ────────────────────────────────────────────────
   @layer, declared below. Order there decides a tie, so intent stops
   depending on which line was typed first.

   One trap layers do not catch: a container that styles bare element
   selectors repaints anything nested inside it. `.settings button` (0,1,1)
   ties `.segset button` (0,1,1) and wins on source order. The answer is the
   child combinator — `.settings > button` — so a container dresses its own
   controls and nothing else. Worth checking whenever a component is nested
   inside one.

   ─── WHERE THE SYSTEM STOPS ──────────────────────────────────────────────
   It governs the page. It does NOT restyle the score: inscriptio emits
   `var(--tonus-note, #111)` and this sheet rebinds three properties, so a
   drawn chant is re-inked without being re-rendered.

   It does not govern the figures' insides either — those obey
   diagrams/ink.js: one ink graded by opacity, one nib, four stroke rungs.
   The two systems agree by SHARING NUMBERS, not by one importing the other,
   which is the seam to watch: a face or a step changed on one side and not
   the other is how two monospaces once ended up on one screen.
--------------------------------------------------------------------------- */

@font-face {
  font-family: "Junicode";
  font-weight: 300 700;
  font-display: swap;
  src: url("fonts/JunicodeVFsubset.woff2") format("woff2-variations");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/IBMPlexMono.woff2") format("woff2");
}
/* The real italic cut, declared as one. The upright file used to BE the italic
   by mistake, which is how the page came to have a slanted subheading — worth
   keeping, so it is now had on purpose and the browser is told which is which
   rather than synthesising a slant. */
@font-face {
  font-family: "IBM Plex Mono";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/IBMPlexMono-Italic.woff2") format("woff2");
}
@font-face {
  font-family: "Jacquard";
  font-weight: 400;
  font-display: swap;
  src: url("fonts/Jacquard24.woff2") format("woff2");
}
/* Jacquard 12 is the same design cut for SMALL sizes — 24's fine strokes fill
   in and its counters close below about 20px. Named separately rather than
   given a size range, because a caller picks the face for the job. */
@font-face {
  font-family: "Jacquard 12";
  font-weight: 400;
  font-display: swap;
  src: url("fonts/Jacquard12.woff2") format("woff2");
}

/* THE CASCADE, DECLARED. Order here is the only thing that decides a tie, so
   intent stops depending on which line was typed first. Two rules were being
   silently overridden before this existed: `.tagline`'s upright, beaten by
   `.sub`'s italic 125 lines later, and `.filters`' 6px gap, beaten by
   `.settings`. Both were correct and both lost to the order of the file. */
@layer tokens, base, layout, components, figures;

@layer tokens {
  :root {
    /* ── the ink ──
       ONE HUE FOR THE WHOLE BOOK. Every neutral is a dilution of the rubrica:
       hue 40, against the rubrica's 28. They used to sit at 85–106 —
       yellow-olive, which is what beige is — 60° from the one colour on the
       page, so the warmth read as a tint the red had nothing to do with.

       And the chroma is a RAMP now. It ran .0000 .0100 .0074 .0151 .0147 down
       the ladder: five independent decisions, with --edge half as warm as both
       its neighbours and --ghost the most saturated neutral on the page at the
       lightness where a tint shows most. That was the muddiness. It follows
       one curve, peaking in the mids and going neutral at both ends, so the
       ink stays ink and the paper stays paper:
         .0079  .0135  .0125  .0093  .0053  .0003
         ink    label  edge   ghost  rule   paper

       EVERY L IS HELD, so every ratio below survives to the second decimal and
       none of the reasoning needed re-measuring. Verified: 18.60, 5.32, 1.99,
       1.37, 3.41. */
    --ink: #14100E;
    --paper: #FDFDFD;
    --rubrica: #9E2B25;
    /* Secondary text — metadata, captions, glosses. 5.32:1, and the rung the
       page reaches for when text should be quiet. */
    --label: #726865;
    /* A HEADING IS NOT AN ASIDE. Chart titles, table captions and the label
       over a control name the thing beneath them, and at --label they read
       as commentary on it — the same weight as the metadata they sit above.
       7.49:1: a clear step darker, and still far nearer --label (5.32) than
       --ink (18.60), because a heading orients and does not speak. Same hue
       family, same low saturation (12.0°/5.9%, against label's 13.8°/6.0%). */
    --heading: #5A5250;
    /* A MARK, NOT TEXT. 1.99:1 — a shade for a thing that is present and not
       claimed: an empty tick, the leading arrow of a jump link, the edge of a
       swatch. It is NOT a text colour, and four rules used to set text in it,
       one compounding with an opacity to 1.43:1 — the hairline-rule ratio.
       Quiet TEXT is --label, and italic is what carries "not claimed" — see
       .ghost, .sub. */
    --ghost: #BCB5B2;
    /* A RULE separates paper — a hairline in a book. 1.37:1 against paper is
       right for it, because it is not asking to be touched. */
    --rule: #DED9D8;
    /* An EDGE is the boundary of a control, and it AFFORDS — so it has to be
       seen. 3.40:1, which is the floor WCAG sets for a non-text control
       boundary. The two were one token, and every field on the page wore a
       separator where it needed an edge. */
    --edge: #918884;
    /* THE PAPER LADDER — three steps, all warm, all off the same hue.
       A selected row stands one step UP, into pure white, so it lifts rather
       than dirties. There is no step DOWN: a figure is given weight by its own
       structure — see STRATUM.rail — not by a panel of tint behind it. */
    --paper-lit: #FFF;

    /* ── the faces ── */
    --serif: Junicode, "Crimson Pro", Georgia, serif;
    --mono: "IBM Plex Mono", ui-monospace, Menlo, monospace;
    --display: Jacquard, var(--serif);
    /* ── the punctum, for a control that marks a note ──
       Bravura's E990, the square notehead, as a mask so it takes currentColor.
       Solid is on, hollow is off — one note, inked or not. The path is the
       same glyph the emitter draws, kept here because CSS cannot reach the
       baked JSON the diagrams import. */
    --punctum-solid: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 160 203%22%3E%3Cg transform=%22translate(0 102) scale(1 -1)%22%3E%3Cpath d=%22M0 -97C0 -101 1 -101 2 -101C4 -101 7 -99 8 -97C20 -90 48 -78 80 -77H85C121 -77 145 -89 153 -97C156 -100 157 -101 158 -101C160 -101 160 -97 160 -97V48C160 83 117 102 76 102C37 102 0 85 0 50Z%22 fill=%22black%22/%3E%3C/g%3E%3C/svg%3E");
    --punctum-hollow: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 160 203%22%3E%3Cg transform=%22translate(0 102) scale(1 -1)%22%3E%3Cpath d=%22M0 -97C0 -101 1 -101 2 -101C4 -101 7 -99 8 -97C20 -90 48 -78 80 -77H85C121 -77 145 -89 153 -97C156 -100 157 -101 158 -101C160 -101 160 -97 160 -97V48C160 83 117 102 76 102C37 102 0 85 0 50Z%22 fill=%22none%22 stroke=%22black%22 stroke-width=%2224%22/%3E%3C/g%3E%3C/svg%3E");

    /* The planetary and musical symbols are in neither text face (see
       fonts/README.md); this is the one place a system stack is correct. */
    --symbol: "Apple Symbols", "Segoe UI Symbol", "Noto Sans Symbols2", var(--serif);

    /* ── the type scale the diagrams draw with ──
       A modular scale on 15px, stepped by the PYTHAGOREAN MINOR THIRD, 32:27
       — three pure fifths up, less an octave, 294.1 cents. Not the tempered
       6/5: this page is about the tuning the chant was sung in, so the type
       climbs by an interval the library can name.

       15 is the base because it is the size most of the page is set at — the
       named things, the table cells, the row headings. Everything else is a
       step from it. Values are rounded to the half-pixel a screen resolves;
       none is more than 0.3px off its exact ratio. */
    --micro: 10.5px;
    --caption: 12.5px;
    --step-label: 15px;
    --body: 18px;
    --title: 21px;
    --display-size: 25px;
    /* The wordmark is the one thing off the scale, and it is off it on
       purpose: a masthead is a picture of a word. Named so it cannot be
       mistaken for a seventh step. */
    --wordmark-size: 36px;

    /* ── tracking: two values, not seven ──
       Tracking is optical, so it falls as size rises: 9px capitals need air
       that 13.5px capitals do not. There were seven values across the sheet
       (.14 .12 .10 .08 .06 .04 .005) for what is really two decisions. */
    --track-micro: 0.09em;   /* capitals at 9px — headings, table heads, labels */
    --track-caps: 0.05em;    /* capitals and small caps at 11px and up */
    --track-meta: 0.04em;    /* mono metadata lines, upright or italic */
    /* Small caps, once `smcp` is in the subset, want less again than capitals
       — the designer already drew them wide. Budget 0.03em and measure. */

    /* ── spacing: one 4px base ──
       There were 26 distinct px values in this file, including both 4 and 5,
       both 8 and 9, and 44 46 48 50 52. A step is now a choice from nine. */
    --sp-1: 4px;
    --sp-2: 8px;
    --sp-3: 12px;
    --sp-4: 16px;
    --sp-5: 20px;
    --sp-6: 24px;
    --sp-7: 32px;
    --sp-8: 48px;
    --sp-9: 64px;
    /* The page's outer air is its own decision, not a step: it is the margin
       of the leaf, and it changes with the width of the window. */
    --page-x: 48px;
    /* THE MEASURE. Past about this width the two columns stop being a spread
       and become two far-apart pages: the chant's staff runs so long the eye
       loses the line it is on, and the reader's head turns to cross the gap.
       The head's rule still spans the window — it is the edge of the leaf, not
       of the text — so the cap goes on the CONTENT inside it. */
    --measure: 1680px;

    /* ── controls ── */
    --control-h: 28px;
    --focus: 2px solid var(--rubrica);
    /* A focus ring sits OUTSIDE a box that has its own edge, and INSIDE
       anything full-bleed — a table row, a list row, a tab — where an outside
       ring would be clipped by the scroll box or collide with the row above.
       That sentence is the whole rule; there is no third case. */
    --focus-out: 2px;
    --focus-in: -2px;
  }
}

@layer base {
  * { box-sizing: border-box; }

  body {
    margin: 0;
    background: var(--paper);
    color: var(--ink);
    font-family: var(--serif);
    font-size: var(--body);
    line-height: 1.5;
  }

  /* ONE link style, worn in the masthead and in every subheader. A dotted
     hairline reads as "there is more here" without drawing a line under half
     the line it follows. It sets its own face rather than inheriting: the
     masthead's siblings are not `.sub`, so relying on inheritance would give
     the two places different type. Colour and text-decoration must be set
     either way, or the UA gives an unvisited link its own blue and underline. */
  .doc {
    font-family: var(--mono);
    font-size: var(--caption);
    font-style: italic;
    color: var(--label);
    text-decoration: none;
    border-bottom: 1px dotted var(--rule);
  }
  .doc:hover { color: var(--ink); border-bottom-color: var(--label); }
  .doc:focus-visible { outline: var(--focus); outline-offset: var(--focus-out); }

  /* The ITALIC is what says "present but not claimed", which is how .sub and
     .chant-row-meta already say it. The colour was saying it a second time, at
     a ratio that stopped saying anything — --ghost is 1.99:1. */
  .ghost { color: var(--label); font-style: italic; }

  .note {
    font-size: var(--caption); color: var(--label);
    max-width: 58ch; line-height: 1.5; margin: var(--sp-3) 0 0;
  }

  /* The house label: what a control or a figure IS, set small and lettered
     out. Defined once, unscoped, because it is worn in several places. */
  .set-name {
    font-family: var(--mono);
    color: var(--heading);
    letter-spacing: var(--track-micro);
    text-transform: uppercase;
    font-size: var(--micro);
  }

  /* The panel subheading is italic — the quiet aside under a title, set apart
     from the upright mono the tables and readouts use. */
  .sub {
    font-family: var(--mono); font-size: var(--caption);
    font-style: italic;
    color: var(--label); letter-spacing: var(--track-meta); margin: 0;
  }
}

@layer layout {
  /* ── the head: wordmark, and the one place Jacquard appears ── */
  .head {
    display: flex;
    align-items: baseline;
    gap: var(--sp-5);
    padding: var(--sp-5) var(--page-x) var(--sp-4);
    flex-wrap: wrap;
    /* Capped with main, so the wordmark and the view switch stay over the
       columns rather than drifting to the window's corners. */
    max-width: var(--measure);
    margin-inline: auto;
    /* THE RULE IS THE LEAF'S EDGE, so it spans the WINDOW while the content
       above it stops at the measure. A border on this box would stop with the
       content; this draws past it on both sides. */
    position: relative;
  }
  .head::after {
    content: "";
    position: absolute;
    left: 50%; transform: translateX(-50%);
    bottom: 0; width: 100vw; height: 1px;
    background: var(--rule);
  }
  .wordmark {
    font-family: var(--display);
    font-size: var(--wordmark-size);
    letter-spacing: 0.04em;
    color: var(--rubrica);
    line-height: 1;
  }
  /* What the thing IS, before the version that says which one. `.views`
     already claims the slack with its own auto margin, so this only sits in
     the line. Upright, though it wears `.sub`: this names the site, and a
     masthead does not lean. The italic belongs to a panel's aside.

     Two classes deep on purpose. As `.tagline` alone this tied with `.sub`
     and lost on source order, so the masthead leant for months against the
     comment directly above it. */
  .sub.tagline { color: var(--label); font-style: normal; }
  /* The version link IS a doc link — it wears `.doc` and adds nothing. */

  /* ── the page's outer air ──
     The header carries the same horizontal inset so its rule starts and stops
     with the content beneath it. */
  main {
    padding: var(--sp-6) var(--page-x) var(--sp-9);
    max-width: var(--measure);
    margin-inline: auto;
  }

  /* Five columns, three and two: the subject gets the wider half because a
     score and a chant list are wide things, and a wheel is not. */
  /* ONE grid for the whole page, not four stacked ones.
     Wide: four rows by two columns, so the two sides align band for band — a
     reader's eye runs level from a title to the title opposite.
     Narrow: the SUBJECT stacks under the subject, the READING under the
     reading. Four independent row-grids each collapsed on their own, which
     interleaved the two columns — the reading's tabs landed above the chant's
     own subtitle, and the temperament slider above the score it does not
     control. A panel is a self-contained unit and has to stack as one. */
  .page {
    display: grid;
    grid-template-columns: 3fr 2fr;
    grid-template-rows: auto auto auto 1fr;
    column-gap: var(--sp-8);
  }
  /* The rows are `display: contents`, so their two cells become direct
     children of the page grid and place themselves in it. The elements stay
     in the DOM for the repaint to find (renderPanels queries .row-title,
     .row-detail and .row-body .cell), but they stop being layout boxes. */
  .row { display: contents; }
  .cell { min-width: 0; }

  /* Each cell takes its band. Column 1 is the subject, column 2 the reading. */
  .row-title > .cell:first-child { grid-area: 1 / 1; }
  .row-title > .cell:last-child { grid-area: 1 / 2; }
  .row-detail > .cell:first-child { grid-area: 2 / 1; }
  .row-detail > .cell:last-child { grid-area: 2 / 2; }
  .row-inputs > .cell:first-child { grid-area: 3 / 1; }
  .row-inputs > .cell:last-child { grid-area: 3 / 2; }
  .row-body > .cell:first-child { grid-area: 4 / 1; }
  .row-body > .cell:last-child { grid-area: 4 / 2; }

  /* ── how the two columns meet across a band ──
     `align-items: end` aligned the two cells' BOXES, which is not what the
     frame promises. A 21px title and a 17px tab strip set to the same box
     bottom still put their two texts on different lines, because each carries
     its own half-leading: measured, the title's baseline sat about 3px below
     the tab's and its box ended 5px lower. A reader's eye runs along a
     BASELINE, so the two header bands align on one. */
  .row-title > .cell,
  .row-detail > .cell { align-self: baseline; }
  /* START, not end. Both inputs cells were one row tall, so aligning their
     bottoms also aligned their controls. Canticum's left cell carries two
     rows now (notatio, then the track strip beneath it), and an end
     alignment set the RIGHT panel's control level with the second row: the
     temperament slider sat beside the tracks rather than beside notatio.
     Aligning the tops puts each column's first control on one line, which
     is what the two columns are for. */
  .row-inputs > .cell { align-self: start; }
  /* The body starts at the top of its band: applied to the body, an end
     alignment sank the short column to the foot of the tall one and left the
     readings a screen and a half below the score. */
  .row-body > .cell { align-self: start; margin-top: var(--sp-5); }

  /* The bands hold their height whatever is in them, so a tab that swaps a
     control row for an empty one — Festum has no inputs where Harmonia has
     two — does not move everything below it by the height of a button. The
     floor is the tallest thing each band can carry.
     Stated on the CELLS, not the rows: the rows are `display: contents` and
     have no boxes of their own to give a height to. */
  .row-title > .cell { min-height: 34px; }
  .row-detail > .cell { min-height: var(--sp-6); padding-top: var(--sp-4); }
  .row-inputs > .cell {
    display: flex; align-items: flex-start; flex-wrap: wrap; gap: var(--sp-2);
    min-height: var(--control-h);
    padding-top: var(--sp-4); padding-bottom: var(--sp-2);
    box-sizing: content-box;
  }

  .row-title h1 { font-size: var(--title); font-weight: 500; margin: 0; }
  /* The chant's title and the way back beside it, on one baseline. */
  .title-row { display: flex; align-items: baseline; gap: var(--sp-4); min-width: 0; }
  .title-row h1 { min-width: 0; }
  .ad-festum {
    font-family: var(--mono);
    font-size: var(--micro);
    letter-spacing: var(--track-meta);
    white-space: nowrap;
  }

  /* Between ~900 and 1200 the 3fr/2fr split starves the readings — the
     right column bottoms out near 380px and the figures stop being
     readable. Here the two columns split EVENLY; past 1200 the subject
     earns its extra width back, and below 900 the page already stacks. */
  @media (max-width: 1199px) and (min-width: 901px) {
    .page { grid-template-columns: 1fr 1fr; }
  }

  @media (max-width: 900px) {
    /* One column, and the bands re-order so each panel is contiguous: the
       subject's title, detail, inputs and body, then the reading's. */
    .page {
      grid-template-columns: 1fr;
      grid-template-rows: repeat(8, auto);
    }
    .row-title > .cell,
    .row-detail > .cell,
    .row-inputs > .cell,
    .row-body > .cell { align-self: start; }
    .row-title > .cell:first-child { grid-area: 1 / 1; }
    .row-detail > .cell:first-child { grid-area: 2 / 1; }
    .row-inputs > .cell:first-child { grid-area: 3 / 1; }
    .row-body > .cell:first-child { grid-area: 4 / 1; }
    .row-title > .cell:last-child { grid-area: 5 / 1; }
    .row-detail > .cell:last-child { grid-area: 6 / 1; }
    .row-inputs > .cell:last-child { grid-area: 7 / 1; }
    .row-body > .cell:last-child { grid-area: 8 / 1; }
    /* The second panel opens with its own tab strip, which needs air above. */
    .row-title > .cell:last-child { margin-top: var(--sp-8); }
    /* The outer air is a luxury of a wide page; on a narrow one the content
       needs the width more than the margin does. */
    :root { --page-x: var(--sp-6); }
  }
}

@layer components {

  /* ═══ RULE 2 · AN UNDERLINE MEANS THIS IS WHERE YOU ARE ═══
     One strip grammar, worn by the view switch, the panel readings, and the
     three date anchors. A name is underlined, not boxed, and the strip carries
     the rule as its own background — so the current name reads as a DARKENING
     of a line that runs the whole width, not as a stroke floating under one
     word.

     Set in Junicode. The details beneath are mono, and a tab in the same face
     reads as more machine data rather than as a name you may choose; the serif
     is also the house's main face and had been left carrying almost nothing. */
  .tabstrip {
    display: flex; gap: var(--sp-6); flex-wrap: wrap;
    background-image: linear-gradient(var(--rule), var(--rule));
    background-size: 100% 2px;
    background-position: 0 100%;
    background-repeat: no-repeat;
  }
  .tabstrip button {
    font-family: var(--serif); font-size: var(--body);
    font-weight: 400; letter-spacing: 0;
    color: var(--label); background: none;
    border: 0; border-bottom: 2px solid transparent;
    padding: 2px var(--sp-1) var(--sp-2); cursor: pointer;
  }
  .tabstrip button:hover { color: var(--ink); }
  .tabstrip button[aria-selected="true"] {
    color: var(--ink); font-weight: 600;
    border-bottom-color: var(--ink);
  }
  .tabstrip button:focus-visible { outline: var(--focus); outline-offset: var(--focus-in); }
  .tabpanel:focus-visible { outline: none; }

  /* The view switch is the same strip at the same SIZE as the other two, set
     in small caps. Choosing a view is the largest choice on the page, but it
     was carrying that in point size and outweighed the masthead beside it;
     small caps mark it as a different KIND of choice instead — a register
     change rather than a louder voice.

     (It was a hand-built row of buttons that copied twenty-six declarations
     from here and shipped `aria-selected` on a plain button with no
     `role="tab"`, no `aria-controls` and no arrow keys — the one strip a
     keyboard reaches first was the one strip that did not behave like a tab
     strip. It comes from `tabs()` now, like the other two.) */
  .tabstrip-large button {
    /* `small-caps`, not `all-small-caps`: the first letter keeps its full cap
       height and only the rest of the word comes down. All-small-caps levels
       the initial too, which loses the word's own shape — Canticum reads as a
       name this way and as a label the other. */
    font-variant-caps: small-caps;
    letter-spacing: var(--track-caps);
    /* Junicode DRAWS its small caps (smcp is in the subset as of 2026-08-10),
       so weight is a real axis here rather than a synthetic thickening of
       scaled capitals. 500 gives the strip presence at body size without the
       selected tab — which goes to 600 — losing its distinction. */
    font-weight: 500;
  }
  /* The chosen VIEW is rubrica — text and rule both. The page's one red marks
     the largest choice on it, the way a rubric marks the thing being sung; the
     panel strips below stay in ink, so the header reads as a different order
     of choice rather than a louder instance of the same one. */
  .tabstrip-large button[aria-selected="true"] {
    color: var(--rubrica);
    border-bottom-color: var(--rubrica);
  }

  /* The strip is aligned to the header's own bottom edge, not to the baseline
     the wordmark and the tagline share: this strip carries a RULE, and a rule
     floating above the header's rule reads as a mistake.

     The pull is the header's bottom padding PLUS its 1px border. Closing only
     the padding left the strip's 2px rule sitting directly on top of the
     header's 1px one — measured, a 3px band under the nav where every other
     rule on the page is 1px. The two now coincide: the strip's rule covers the
     header's, and the current view darkens it. */
  .views {
    margin-left: auto;
    align-self: flex-end;
    margin-bottom: calc(-1 * (var(--sp-4) + 1px));
  }

  /* ── the chant row: a list entry with its own notation ── */
  .chant-list { list-style: none; margin: 0; padding: 0; }
  .chant-row { border-bottom: 1px solid var(--rule); }
  /* Name and rubric on the left where a list is scanned, the notation on the
     right where it is checked.
     The music STARTS at the 40% mark and runs to the edge. That means the LEFT
     side is the fixed half — sizing the right column instead lets the gaps
     shift where the music begins, and the column stops reading as notation and
     starts reading as ragged fragments. Two columns exactly, with the gap
     folded out of the first, so the staff's left edge is a true 40%.

     The percentage resolves against the CONTENT box, so any horizontal padding
     would shift that mark and make it drift with the row's width. There is
     none: the row's air is vertical, and the text column carries its own left
     inset instead. Verified exact from 420px to 1100px — do not fold these
     14px into the spacing scale without re-measuring. */
  .chant-row button {
    display: grid;
    grid-template-columns: calc(40% - 14px) 1fr;
    align-items: center; gap: 14px;
    width: 100%; text-align: left;
    background: none; border: 0; padding: var(--sp-3) 0;
    font: inherit; color: var(--ink); cursor: pointer;
  }
  /* The name takes the slack, the aside sits at the half's right edge. The
     left inset lives here rather than on the button, so the row's padding
     cannot shift where the notation starts.
     It is --sp-3 to match a table cell's: both wear the same selection bar on
     their left edge, so the text has to stand off it by the same air, or the
     two marks read as different marks. */
  .chant-row-head {
    display: flex; align-items: baseline; gap: var(--sp-3);
    min-width: 0; justify-content: space-between; padding-left: var(--sp-3);
  }
  .chant-row-head .chant-row-text { min-width: 0; }
  .chant-row button:focus-visible { outline: var(--focus); outline-offset: var(--focus-in); }

  /* ── SELECTION, IN ONE CHANNEL ──
     A list of chants is a list of CHOICES, so the ones not taken step back to
     label grey and the one taken comes forward in full ink. The row's own rule
     carries the same news: --rule at rest, --edge under the pointer, --ink
     where you are. No fill. A fill also fought the incipit drawn in the same
     row, which is the second reason it is gone.

     If a fill is ever wanted back for a denser list, the replacement is a 2px
     ink bar in the left margin — NOT a tint. */
  /* The name is the row's SUBJECT, so it carries full ink at rest. At --label
     it read as one more field of the caption below it — ranked under its own
     metadata, which is backwards for the thing you choose by. Measured against
     the paper: --label is 5.29:1, --ink 18.55:1.
     COLOUR IS THEREFORE SPENT. All three states share one ink, and the climb
     is carried by WEIGHT and by the margin bar instead — which is the sheet's
     own rule: selection is marked, not filled. */
  .chant-row-name {
    display: block; font-size: var(--step-label);
    color: var(--ink);
    font-weight: 400;
  }
  .chant-row { position: relative; }
  /* ONE MARK IN THE MARGIN, for both states, as the tables wear it: a bar the
     height of the row, outside the text column in the air the page's own inset
     provides. Hover used to darken the row's BOTTOM rule, which put the answer
     to the pointer on the boundary between two rows — at a glance it reads as
     belonging to the row below as readily as the one above. A left bar belongs
     to exactly one row.

     ON THE ROW'S OWN EDGE, which is where the tables put theirs. It used to
     hang at -12px, out in the page's inset — so the two marks disagreed by a
     full 12px against the text they mark, and the chant list's bar read as
     floating off to the left of its own row.

     Nothing moves and nothing fills: the bar is absolutely positioned, so it
     costs the row no width and the list does not shift under the pointer. The
     name also gains a little weight — Junicode's wght axis is real, so 500 is
     a drawn weight and the advance does not reflow. */
  .chant-row::before {
    content: "";
    position: absolute; left: 0; top: 0; bottom: 0;
    width: 2px; background: transparent;
  }
  .chant-row:hover::before { background: var(--edge); }
  .chant-row button:hover .chant-row-name { font-weight: 500; }
  /* SELECTED is the same bar in RUBRICA — the page's one red, spent on the
     single thing the reader has chosen — with the row on pure white so it
     lifts off the page rather than being washed with a tint. Hover is the grey
     preview of exactly this mark, in the same place at the same width. */
  .chant-row[aria-selected="true"]::before { background: var(--rubrica); }
  /* The chosen name keeps the weight the hover previewed. No colour change —
     it already carries full ink, and the margin bar is what marks the row. */
  .chant-row[aria-selected="true"] .chant-row-name { font-weight: 500; }

  /* The incipit is one long system, shown from its start and clipped: a few
     notes of the real melody, which is how a chant is recognised. The box is
     the full height of the render at this scale — clipping it shorter cut the
     lyric line off under the staff. The mask fades the cut so the music trails
     away rather than ending on a hard edge mid-neume. */
  .chant-row .incipit {
    display: block; overflow: hidden;
    height: 50px;
    /* #000 here is an ALPHA CHANNEL, not a colour: a mask reads opacity, so
       any opaque value behaves identically and --ink would render the same.
       Said out loud because it otherwise looks like the one token bypass in
       the sheet, and someone will "fix" it. */
    mask-image: linear-gradient(to right, #000 70%, transparent);
  }
  .chant-row .incipit svg { height: 50px; width: auto; max-width: none; display: block; }
  /* THE OFFICE, BOXED OVER THE MUSIC'S TAIL. The incipit masks away to
     transparent from 70%, so the row's last inch is already white — the box
     sits in that white rather than over any notation. Positioned, never a
     third grid column: the column widths above are load-bearing for where
     the staff begins. */
  .chant-row-music { position: relative; min-width: 0; }
  .chant-row-cat {
    /* ON THE SUBLINE, not on the row's centre. It reads as one more field of
       the metadata — the office, beside the genus and the mode — so it lines
       up with the line it belongs to rather than floating at the midpoint of
       a 50px staff. `bottom` rather than `top`, because the subline sits
       below centre and the box grows upward from its own baseline. */
    position: absolute; bottom: 2px;
    right: var(--sp-5);
    /* The subline's dress, one step down — it is the same register of fact,
       so it is not given a face of its own. */
    font-family: var(--mono); font-size: var(--micro); font-style: italic;
    letter-spacing: var(--track-meta); color: var(--label);
    white-space: nowrap;
    padding: 2px var(--sp-2);
    border: 1px solid var(--rule);
    background: var(--paper);
  }
  /* Italic, as the panel subheadings are: this is the same kind of line — the
     quiet aside under a name. */
  .chant-row-meta {
    display: block; font-family: var(--mono); font-size: var(--micro);
    font-style: italic;
    color: var(--label); letter-spacing: var(--track-meta); margin-top: 2px;
  }
  /* ── how to read the tracks: the same spur, a card of instruction ── */
  .tracks-sample { display: block; margin-bottom: var(--sp-1); }
  .tracks-name {
    font-family: var(--mono); font-size: var(--micro);
    letter-spacing: var(--track-micro); text-transform: uppercase;
    color: var(--label); margin: 0 0 var(--sp-1);
  }
  /* The tracks card's prose is the same thing .key-desc is — the paragraph
     that opens a popover — so it takes the same dress. */
  .tracks-text {
    font-family: var(--mono);
    font-style: italic;
    font-size: var(--micro); line-height: 1.65;
    letter-spacing: var(--track-meta);
    color: var(--label); margin: 0;
    /* 40ch was set for the serif. A mono ch is wider, so the same count
       wrapped these paragraphs well short of the entries beside them and the
       card read as two ragged columns. The card's own max-width is the
       measure now, and `pretty` keeps a lone word off the last line. */
    max-width: 70vw;
    text-wrap: pretty;
  }
  /* A block may need a second paragraph (the tonarium draws four things and
     one paragraph buries the last two). Space them, or they set as one. */
  .tracks-text + .tracks-text { margin-top: var(--sp-2); }
  /* THE GAP BETWEEN BLOCKS. Up to three tracks stack in one card, each opening
     with its sample; the space before that sample is what separates one track
     from the last. A block can end in prose OR in a run of mark entries, so
     both hand off to the next — matching on `.tracks-sample` alone would set
     the gap after prosodia's entries and nowhere else. */
  .tracks-text + .tracks-sample,
  .key-entry + .tracks-sample { margin-top: var(--sp-5); }

  .chant-row-aside {
    font-family: var(--mono); font-size: var(--caption);
    color: var(--label); font-variant-numeric: tabular-nums;
  }

  /* ═══ RULE 1 · A BOX MEANS A VALUE LIVES HERE ═══
     The date, typed to arrive and stepped to wander, is ONE object: a field
     with two spurs, hairline-divided, inside a single edge. It was three
     detached boxes at three type sizes — 13.5px arrows around an 11px date —
     which read as three unrelated controls for what is one reading of one
     value. */
  .dials { display: flex; align-items: center; gap: var(--sp-6); flex-wrap: wrap; }

  .field {
    height: var(--control-h);
    display: inline-flex; align-items: stretch;
    border: 1px solid var(--edge);
    background: var(--paper);
  }
  .field:focus-within { outline: var(--focus); outline-offset: var(--focus-out); }
  .dial-date {
    border: 0; background: none;
    font-family: var(--mono); font-size: var(--caption);
    color: var(--ink); padding: 0 var(--sp-3);
    font-variant-numeric: tabular-nums;
  }
  .dial-date:focus-visible { outline: none; }
  /* A spur DOES something and holds nothing, so it wears no edge of its own —
     only the hairline that divides it from the value it steps. */
  .dial-step {
    border: 0; border-left: 1px solid var(--rule); background: none;
    font-family: var(--mono); font-size: var(--step-label); line-height: 1;
    color: var(--label); width: 26px; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer;
  }
  .field > .dial-step:first-child { border-left: 0; border-right: 1px solid var(--rule); }
  .dial-step:hover { color: var(--ink); }
  .dial-step:focus-visible { outline: var(--focus); outline-offset: var(--focus-in); }

  /* ── the anchors: three JUMP LINKS ──
     Nativitas, Pascha and Pentecoste hold no value and set nothing — they are
     the three days the year is reckoned from, and going to one is going
     somewhere. Boxed, they were indistinguishable from the office filters
     below; given the tab grammar they claimed to be a third view switch,
     which they are not — a tab strip says "one of these is showing", and
     these are three places you may go.

     So: little links, in the page's existing link idiom — the dotted hairline
     `.doc` already uses to say "there is more here" — with a leading mark for
     the jump. The one you are standing on takes full ink and a solid rule. */

  /* ═══ RULE 3 · A SET IS ONE OBJECT MADE OF PARTS ═══
     The five offices are independently on or off, and together they decide one
     thing: what the list below holds. Five detached boxes said "five unrelated
     actions". One strip with internal hairlines says what is true.

     The tick is a second channel, so membership is not carried by colour
     alone — which matters here because the whole state of this control used to
     live in a border at 1.37:1 against paper. */
  .segset {
    display: inline-flex; flex-wrap: wrap;
    border: 1px solid var(--edge);
    min-height: var(--control-h);
    margin-bottom: var(--sp-3);
  }
  /* A strip inside a control row carries no bottom margin: the row owns the
     air beneath it. Left on, flex centres a 40px box (28 + 12) against the
     spur's 18 and the ? sits 6px low — half the margin, exactly. */
  .settings .segset,
  .track-strip .segset { margin-bottom: 0; }
  /* The officia strip and the hours' spur ride one line: the spur belongs to
     the Officium segment, so it sits with it rather than under the set. */
  .officia-row { display: flex; align-items: center; gap: var(--sp-2); }
  .officia-row .segset { margin-bottom: 0; }
  .officia-row .key-info { float: none; }
  /* One hour per line in the card: the box, the name, and what it would add.
     The count is right-aligned on its own column so eight numbers of different
     widths still make a column rather than a ragged edge. */
  .hora-row {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: var(--sp-2);
    padding: 2px 0;
    cursor: pointer;
  }
  .hora-row input[disabled] ~ .hora-name { color: var(--ghost); }
  .hora-name { font-size: var(--caption); }
  .hora-n { font-family: var(--mono); font-size: var(--micro); }
  .segset button {
    font-family: var(--mono); font-size: var(--caption);
    letter-spacing: var(--track-meta);
    /* Membership is carried by the TICK (a second channel, as the note above
       says) and by the climb to --ink on selection. The name does not also
       have to be dimmer than legible to say "not chosen": --ghost is 1.99:1,
       and these are the labels of an interactive control. */
    color: var(--label); background: none;
    border: 0; border-left: 1px solid var(--rule);
    padding: 0 var(--sp-3); min-height: 26px;
    display: inline-flex; align-items: center; gap: var(--sp-2);
    cursor: pointer;
  }
  .segset button:first-child { border-left: 0; }
  /* THE MARK IS A PUNCTUM — the square notehead the chant books print, and the
     one this page's own emitter draws. It was a 5x5 CSS box, which is not the
     shape: a punctum is TALLER than it is wide (160x203 font units, 0.788) and
     its edges are drawn, not ruled. Off is the same note hollow.

     Carried as a mask rather than a background, so `currentColor` still drives
     it and the mark follows the button's ink through hover, pressed and
     disabled without restating a colour. The path is Bravura's E990, the same
     glyph tonus bakes for the notation. */
  .segset button::before {
    content: ""; width: 7px; height: 9px; flex: none;
    /* A hair below the line box's middle. Centred exactly, the mark read as
       floating above the word beside it — the glyph carries more ink above its
       waist than below, so its optical centre sits higher than its box's. */
    transform: translateY(1px);
    background: currentColor;
    -webkit-mask: var(--punctum-hollow) center / contain no-repeat;
    mask: var(--punctum-hollow) center / contain no-repeat;
  }
  /* How many the office holds. A count is machine data about the name beside
     it, so it stays quiet whether the segment is in or out. */
  .seg-n { color: var(--label); font-size: var(--micro); font-variant-numeric: tabular-nums; }
  .segset button:hover { color: var(--ink); }
  .segset button[aria-pressed="true"] { color: var(--ink); }
  .segset button[aria-pressed="true"]::before {
    -webkit-mask-image: var(--punctum-solid);
    mask-image: var(--punctum-solid);
  }
  /* TWO CHANNELS STACKED ON ONE STATE is what produced 1.43:1 — --ghost at
     opacity .55 composites to about the hairline-rule ratio, so a disabled
     office name was as visible as a separator. A disabled control is exempt
     from the contrast minimum, but the exemption is not a reason to make it
     invisible: --edge is the 3.41:1 rung, and the tick going empty is what
     actually says "unavailable". */
  .segset button[disabled] { color: var(--edge); cursor: default; }
  .segset button[disabled]:hover { color: var(--edge); }
  .segset button:focus-visible { outline: var(--focus); outline-offset: var(--focus-in); }

  /* ── settings: the few the toy carries ── */
  .settings {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-5);
    align-items: center;
    font-family: var(--mono);
    font-size: var(--caption);
    color: var(--label);
    padding: 0;
  }
  /* A settings block that stacks: each row keeps the flex line the flat
     `.settings` gives, and the rows sit one under the other. */
  /* The track strip, where Calendarium's office strip sits: in the body,
     under the inputs band, with the same air beneath it. */
  .track-strip {
    /* CENTER, and the spur is a flex ITEM here — not the floated thing that
       hangs inside an h2. Its `position: relative` box is 27px tall against
       the strip's 28, and without this it sat 6px low against the buttons. */
    display: flex; align-items: center; gap: var(--sp-2) var(--sp-5);
    position: relative;
    margin-bottom: var(--sp-5);
  }

  .settings label { display: inline-flex; align-items: center; gap: var(--sp-2); cursor: pointer; }
  /* A select HOLDS a value, so it keeps its box — rule 1. The chevron is drawn
     from two gradients rather than left to the OS: the native one and the date
     picker's calendar glyph were the only system-drawn marks on a page that
     draws everything else itself. */
  .settings > select,
  .settings-row > select,
  .settings > button,
  .settings-row > button {
    font-family: var(--mono);
    font-size: var(--caption);
    color: var(--ink);
    background: var(--paper);
    border: 1px solid var(--edge);
    height: var(--control-h);
    box-sizing: border-box;
    padding: 0 var(--sp-3);
    cursor: pointer;
  }
  /* SING — the one action on the site, and the only control here that is not a
     value or a place. COSTUME RULE 3 dresses it: no box, because a box would
     say a value lives in it, and no fill, because nothing on this page changes
     a background. What is left is the word, in the caps the tracks key uses,
     coming up to full ink under the pointer like a row does.

     It sits in a .settings row whose every other child wears an edge, so the
     border is removed rather than never set — the rule above claims it first. */
  .settings > button.sing {
    border: none;
    background: none;
    padding: 0 var(--sp-2);
    color: var(--label);
    text-transform: uppercase;
    letter-spacing: var(--track-caps);
  }
  .settings > button.sing:hover { color: var(--ink); }
  /* Singing is not a costume change: the label already says what the next
     press does, and a control that restyles itself mid-performance reads as a
     different control. Only the weight comes up, the way a chosen row's does. */
  .settings > button.sing[aria-pressed="true"] {
    color: var(--ink);
    font-weight: 600;
  }
  /* THE BENCH — the maker's panel, behind ?schola=true. It borrows the comma
     slider's track and readout wholesale rather than inventing a second kind
     of slider: this page has one, and a workshop is not a reason for a second.
     What it adds is a label column and a gloss, because a bench has more rows
     than a reader's control does and they need naming. */
  .bench-row {
    display: grid;
    grid-template-columns: 6.5rem auto 3rem 1fr;
    align-items: center;
    gap: var(--sp-2);
  }
  .bench-row > label {
    font-family: var(--mono);
    font-size: var(--caption);
    color: var(--heading);
  }
  .bench-gloss {
    font-size: var(--caption);
  }
  /* The list's mark is an INCLINATUM — the diamond notehead, E991. It was two
     linear-gradients cut on the diagonal to fake a triangle, a shape this page
     has no other use for; the toggles beside it already mark state with a
     punctum, so the two controls now speak one notation.

     A select cannot carry a pseudo-element, so the diamond is painted as the
     background rather than masked: the colour is therefore stated once here
     and again on hover, which is the price of putting a mark on this element
     at all. It sits in the padding the select already reserved. */
  .settings > select,
  .settings-row > select {
    appearance: none; -webkit-appearance: none;
    padding-right: var(--sp-7);
    background-image: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 150 226%22%3E%3Cg transform=%22translate(0 113) scale(1 -1)%22%3E%3Cpath d=%22M0 0C0 -1 0 -4 2 -6L69 -106C71 -110 74 -113 75 -113C77 -113 78 -110 82 -106L149 -5C150 -2 150 -1 150 0C150 2 149 5 148 7C144 12 81 109 81 109C78 112 76 113 75 113C72 113 71 112 70 109C70 109 5 10 3 6C0 4 0 2 0 0Z%22 fill=%22%23111111%22/%3E%3C/g%3E%3C/svg%3E");
    background-position: right var(--sp-3) center;
    background-size: auto 9px;
    background-repeat: no-repeat;
  }
  .settings > button:hover,
  .settings-row > button:hover,
  /* Hover darkens the EDGE. The diamond does not change: it already rests at
     full ink, and a second copy of the same image here would be 400 characters
     of URI restating what the resting rule says. */
  .settings > select:hover,
  .settings-row > select:hover { border-color: var(--ink); }
  /* A setting that is ON darkens its edge AND its text — two channels, so the
     state does not live in colour alone. */
  .settings > button[aria-pressed="true"],
  .settings-row > button[aria-pressed="true"] { border-color: var(--ink); color: var(--ink); font-weight: 600; }
  .settings > select:focus-visible,
  .settings-row > select:focus-visible,
  .settings > button:focus-visible,
  .settings-row > button:focus-visible { outline: var(--focus); outline-offset: var(--focus-out); }

  /* ── the temperament slider ──
     One axis, not six names: the fraction of the comma taken off each fifth,
     with the historical stops named in the list beside it.

     ON THE SYSTEM'S TERMS. The track is a control's own rail, so it takes
     --edge and not --rule — you drag on it, and a boundary you touch has to
     be seen. The box round the list is the same box every select wears, and
     the readout is machine data, so it is mono and tabular like every other
     number on the page.

     The BEHAVIOUR is untouched: no snap, COMMA_STEP resolution, and a readout
     that is always cents. See commaSlider() in app.js — the reasoning for each
     is there, and none of it depends on anything here. */
  .comma { gap: var(--sp-2) var(--sp-3); }
  /* The rail's own box. Its width is a control dimension, not a step: it is
     the resolution the axis is read at, and every pixel spreads the seven
     named stops further apart. */
  .comma-track {
    position: relative;
    display: inline-block;
    width: 132px;
    height: var(--control-h);
    flex: none;
  }
  .comma-range {
    -webkit-appearance: none; appearance: none;
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    background: none; cursor: ew-resize; margin: 0;
  }
  .comma-range::-webkit-slider-runnable-track { height: 1px; background: var(--edge); }
  .comma-range::-webkit-slider-thumb {
    -webkit-appearance: none; appearance: none;
    width: 11px; height: 11px; border-radius: 50%;
    background: var(--ink); margin-top: -5px;
  }
  .comma-range::-moz-range-track { height: 1px; background: var(--edge); }
  .comma-range::-moz-range-thumb {
    width: 11px; height: 11px; border: 0; border-radius: 50%; background: var(--ink);
  }
  .comma-range:focus-visible { outline: var(--focus); outline-offset: var(--focus-out); }
  /* The width of the tempered fifth, in cents, everywhere on the axis — so it
     is a number, and numbers on this page are mono and tabular. The width is
     held so the row does not shuffle as digits change. */
  .comma-read {
    font-family: var(--mono); font-size: var(--caption);
    color: var(--ink);
    min-width: 4.5ch;
    font-variant-numeric: tabular-nums;
  }
  .comma select { max-width: 15rem; }
  /* The list reads the axis in the page's own state vocabulary.
     AT a named stop: ink text, ink edge — two channels, the same way every
     other setting says it is on.
     NEAR one: the ghost the page uses for a thing that is present but not
     claimed, because the name is being shown for orientation. The reader can
     still open the list and choose it; only the display is quiet. */
  .comma select.at-detent { color: var(--ink); border-color: var(--ink); }
  .comma select.near-detent {
    /* The quiet BOX (--rule, not --edge) and the italic already carry "shown
       for orientation, not claimed". This is a select's own value: a reader
       has to be able to READ the name they are being oriented by. */
    color: var(--label);
    border-color: var(--rule);
    font-style: italic;
  }

  /* ── panels ── */
  .panel { margin-bottom: var(--sp-7); }
  .panel > h2 {
    font-family: var(--mono);
    font-size: var(--micro);
    letter-spacing: var(--track-micro);
    text-transform: uppercase;
    color: var(--heading);
    font-weight: 400;
    margin: 0 0 var(--sp-3);
    padding-bottom: var(--sp-2);
    border-bottom: 1px solid var(--rule);
  }

  /* The census neighbours, under the score. */
  .similar { display: block; margin-top: var(--sp-8); }
  /* Titled as the readings opposite are titled — the same face, size and rule
     — because it is the same kind of thing: a named section of a column. It
     wears the tabstrip's underline without being a tab, since there is only
     one, and it draws that underline the way the strip does rather than with a
     border, so the two cannot drift. */
  .similar-title {
    font-family: var(--serif); font-size: var(--body);
    font-weight: 600; letter-spacing: 0;
    color: var(--ink);
    margin: 0; padding: 2px 0 var(--sp-2);
    background-image: linear-gradient(var(--rule), var(--rule));
    background-size: 100% 2px;
    background-position: 0 100%;
    background-repeat: no-repeat;
  }
  .similar .sub { margin-top: var(--sp-3); }
  /* The subheader and the list each take their own air — the rows were
     sitting straight on the line above. */
  .similar .chant-list { margin-top: var(--sp-4); }

  /* ── the score ── */
  /* No sideways scroll: the score is re-rendered at the column's width rather
     than overflowing it (see scoreFigure's ResizeObserver). */
  /* The emitter draws its own page margin — a fixed 12px inset, which belongs
     to the notation's page and is right in a printed score. On a web column
     the page margin is already the column's, so the two stack and the chant
     floats inside a gutter it does not need. Pulled back out here, where the
     page's own air is decided; the render is untouched.

     LEFT ONLY. Pulling both sides widened the wrapper by 24px past its cell,
     which is the horizontal scroll: the render is measured to the column, so
     there is nothing on the right to reclaim — the emitter's gutter is at the
     start of the staff, not after it. */
  /* NO TOP MARGIN. This is the first thing in the left column, and 4px here
     collapsed up through its wrapper and pushed the whole column 4px below the
     right one — so Canticum's figures sat lower than Calendarium's, where the
     left cell has no score in it. The bottom margin still holds it off what
     follows. */
  .score { margin: 0 0 var(--sp-2) -12px; }
  /* The chant follows the page's ink. inscriptio emits its colours as
     `var(--tonus-note, #111)` — the render's own value as the fallback — so
     setting these three properties rethemes a drawn score without re-rendering
     it, and the site's tokens become the chant's tokens. */
  .score svg {
    max-width: 100%; height: auto; display: block;
    --tonus-note: var(--ink);
    --tonus-staff-line: var(--ink);
    --tonus-rubrica: var(--rubrica);
  }
}

@layer figures {
  /* ── the tabula, shared by every panel ── */
  /* A wide table scrolls inside its own box; the page never does.
     This lives on the PANEL, not on `.cell`. On the cell it also made the
     block axis a scroll container — CSS cannot leave one axis visible when the
     other is `auto` — which quietly forbids `position: sticky` inside a column
     and clips anything a figure ever wants to draw outside its box. */
  .tabpanel { overflow-x: auto; }

  table.tabula { border-collapse: collapse; width: 100%; font-size: var(--step-label); }
  /* The caption belongs to the table BENEATH it. At sp-3 it floated midway
     between its own rows and the table above, reading as either's; the air
     now sits above the caption (the inter-table margin), not under it. */
  /* THE CAPTION IS A WORD, SO IT IS SERIF. "extent", "melisma", "range" name
     the group beneath them; mono is the face for machine data, and these are
     not that. Junicode draws real small caps (smcp), so this is a cut and not
     scaled capitals — which is also why `text-transform` is gone: uppercasing
     the text first leaves smcp nothing to act on.

     `small-caps`, not `all-small-caps`, matching .tabstrip-large: the initial
     keeps its cap height and the word keeps its own shape. A step up from
     --micro because small caps read smaller than capitals at one size. */
  /* THE PANEL SUBHEADER'S DRESS, which is what .sub already wears: the small
     caps were the one place on the site using that cut, and a style used once
     is a style that has to justify itself. This is the same kind of line as a
     panel's aside, so it is set the same way. */
  table.tabula caption {
    font-family: var(--mono);
    font-style: italic;
    font-size: var(--caption);
    letter-spacing: var(--track-meta);
    color: var(--heading); text-align: left;
    /* THE CAPTION BELONGS TO THE TABLE BENEATH IT, so the air above it (the
       inter-table margin) stays larger than the air below: that difference is
       what assigns it downward rather than leaving it floating between two
       tables. Still negative, because the line box carries descender space
       the caption's letters never reach into — -9px closed the gap entirely
       and set the first row hard against the word, which read as a heading
       row of the table rather than a name for it. */
    padding-bottom: 0;
    margin-bottom: -3px;
  }
  table.tabula th {
    font-family: var(--mono); font-size: var(--micro); letter-spacing: var(--track-micro);
    text-transform: uppercase; color: var(--heading); text-align: left;
    font-weight: 400; white-space: nowrap;
    padding: 0 var(--sp-3) var(--sp-2); border-bottom: 1px solid var(--rule);
  }
  /* Air on BOTH sides of a cell, not just after it. */
  table.tabula td {
    padding: var(--sp-1) var(--sp-3); border-bottom: 1px solid var(--rule);
    vertical-align: baseline;
  }
  table.tabula tr[tabindex] { cursor: pointer; }
  /* ONE MARK, IN THE LEFT MARGIN, for both states — the bar the chant list
     wears. Hover used to darken the row's BOTTOM rule, which put the answer to
     the pointer on the boundary between two rows: at a glance it reads as
     belonging to the row below as easily as the one above. A left edge belongs
     to exactly one row.

     It rides the first cell's inner edge as a shadow rather than a border,
     because a table has no outer inset to hang a bar in and a shadow takes no
     width — so nothing shifts when the mark appears. */
  table.tabula tr[tabindex]:hover td:first-child {
    box-shadow: inset 2px 0 0 var(--edge);
  }
  /* SELECTED is the same bar in RUBRICA: the page's one red, spent on the
     single thing the reader has chosen. Hover is a grey preview of it, in the
     same place and at the same width, so the pointer shows where the mark will
     land before it lands. */
  table.tabula tr.sel td:first-child { box-shadow: inset 2px 0 0 var(--rubrica); }
  table.tabula tr[tabindex]:focus-visible { outline: var(--focus); outline-offset: var(--focus-in); }

  table.tabula .mono { font-family: var(--mono); font-size: var(--caption); }
  table.tabula .num { text-align: right; font-variant-numeric: tabular-nums; }

  /* THE GAMUT LETTERS, in plain Junicode: Γ A B C D E F G then a b c d, one
     gamut where the case marks the OCTAVE. The lowercase keeps its ordinary
     x-height — Junicode's ENLA axis was raising it toward cap height here,
     which spelled the medieval convention but read as an effect at this size. */
  .litera {
    font-family: var(--serif);
    font-size: var(--step-label);
    letter-spacing: 0.04em;
  }
  /* The two b's are MARKS, so they are engraved rather than set: Bravura's
     standard ♭ and ♮ as inline SVG, matching the page's weight instead of
     whatever a text font carries. The glyph box is trimmed to its ink, which
     costs two corrections — the cell's tracking would hang off its right edge
     as a phantom space, and a box taller than a letter's cap height rides ~4px
     high. Measured: this seats the glyph on the same baseline as every letter
     in the column. */
  .litera-glyph {
    display: inline-block;
    vertical-align: -0.445em;
    letter-spacing: 0;
    color: inherit;
  }

  /* A gloss is a WORD — a role (finalis, tenor), a season, a Greek string-name
     — not machine data. So it takes the serif, small and quiet, rather than
     the mono the numeric columns use. Upright, since the mono face carries an
     italic that a Latin term should not inherit.

     A GLOSS TRAVELS WHOLE. "parhypate hypaton" is one name for one string, and
     a cell too narrow for the datum and the name together used to break it in
     the middle of itself — the datum kept its line and the gloss lost a word to
     the next one. So it does not wrap internally; the break goes in FRONT of it
     instead (tabula.js puts a zero-width space there), and a gloss that will
     not fit drops to its own line entire. */
  table.tabula .gloss {
    font-family: var(--serif);
    font-size: var(--caption);
    font-style: normal;
    letter-spacing: 0;
    color: var(--label);
    margin-left: 0.5em;
    white-space: nowrap;
  }
  table.tabula .symbol, table.tabula .pair-grid { font-family: var(--symbol); }
  table.tabula .symbol {
    font-size: var(--step-label);
    line-height: 1;
    white-space: nowrap;
    width: 1%;
  }
  /* Two glyphs and a dot on a fixed grid: the glyph columns are equal and the
     dot's is only as wide as it needs, so the separator falls on one vertical
     down the whole column. Planetary glyphs differ in width by a third or more
     — flowed as text, the dot lands somewhere new on every row. */
  table.tabula .pair { width: 1%; white-space: nowrap; }
  /* The grid rides on a span inside the cell, not on the cell: a td set to
     `display: grid` leaves the table's layout altogether, taking its column
     sizing and its bottom rule with it. */
  table.tabula .pair-grid {
    display: inline-grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: baseline;
    gap: 0 0.45em;
    font-size: var(--step-label);
    line-height: 1;
  }
  table.tabula .pair-grid > span:first-child { text-align: right; }
  table.tabula .pair-grid > span:last-child { text-align: left; }
  table.tabula .pair-grid .pair-dot { color: var(--label); font-size: var(--caption); }
  /* Two tables stacked in one panel need air between them, or the second reads
     as more rows of the first. */
  table.tabula + table.tabula { margin-top: var(--sp-7); }
  /* THE FIRST TABLE CLEARS THE TITLE'S RULE. A figure under a heading carries
     its own margin of white, but a table opens on a caption — so its small
     caps landed a few pixels under the rule and read as attached to it. Only
     the first: the tables after it have --sp-7 between them already. */
  .panel > h2 + table.tabula { margin-top: var(--sp-2); }
  /* A figure and the table under it are one thought; the gap is smaller. */
  svg + table.tabula { margin-top: var(--sp-4); }

  /* ── the sigla: what a figure's marks mean, on request ── */
  /* One idiom: a thin-ringed ? at the right of the figure's title line,
     opening a card of entries — the drawn mark, then its word. The ring is
     not a box (no value lives here); it is the one affordance that says
     "an answer waits behind me". The tracks' card wears the same dress. */
  /* A hair of air around the spur: the tabpanel is a scroll box, and a
     focus ring drawn OUTSIDE a spur flush with its edge was clipped. */
  .key-info { position: relative; float: right; margin: 2px 4px 0 0; }
  .key-spur {
    width: 18px;
    height: 18px;
    padding: 0;
    border: 1px solid var(--edge);
    border-radius: 50%;
    background: none;
    cursor: pointer;
    font-family: var(--mono);
    font-size: var(--micro);
    line-height: 1;
    color: var(--label);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .key-spur:hover { color: var(--ink); border-color: var(--ink); }
  .key-spur:focus-visible { outline: var(--focus); outline-offset: var(--focus-out); }
  .key-card {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    z-index: 10;
    background: var(--paper);
    border: 1px solid var(--edge);
    padding: var(--sp-4);
    width: max-content;
    max-width: 58ch;
    /* The card hangs INSIDE the h2, so the title's costume — caps, mono,
       tracking — would flow into it. A card is prose, not a heading. */
    font-family: var(--serif);
    font-weight: 400;
    letter-spacing: 0;
    text-transform: none;
  }
  .key-card[hidden] { display: none; }
  /* In a CONTROL ROW the card hangs from the left edge instead — its spur
     sits mid-page, not at a column's right, so a card anchored right runs off
     the window. (Measured on the track strip: left −122px.)
     The float goes too. It is there for the spur that hangs inside an h2, and
     in a flex row it drops the spur out of the line: the strip centred at 254
     and the spur at 261. */
  .settings .key-info,
  .track-strip .key-info { float: none; margin: 0; display: flex; align-items: center; }
  .settings .key-card,
  .track-strip .key-card { left: 0; right: auto; }
  /* MONO, UPRIGHT. This is the card's opening statement and the longest prose
     on it, and small italic Junicode is the hardest setting to read at length:
     the slant plus the serif's fine strokes at --caption cost more than the
     "aside" the italic was buying. The mono is a size down, and upright, so
     the paragraph is legible and still sits apart from the serif entries
     under it. */
  .key-desc {
    font-family: var(--mono);
    /* Plex Mono's real italic cut, declared at the top of this sheet. */
    font-style: italic;
    font-size: var(--micro);
    letter-spacing: var(--track-meta);
    color: var(--ink);
    line-height: 1.65;
    margin: 0 0 var(--sp-3);
    /* Balances the last line rather than leaving one word alone on it. */
    text-wrap: pretty;
  }
  .key-entry {
    font-family: var(--serif);
    font-size: var(--caption);
    color: var(--label);
    line-height: 1.6;
    margin: 0;
    /* A wrapped line returns under the WORD, not under the swatch. */
    padding-left: calc(16px + 0.4em);
    text-indent: calc(-16px - 0.4em);
  }
  /* Inline BY SPECIFICITY: `.tabpanel svg { display: block }` outranks a
     bare class, and a block swatch put every mark on its own line above
     its word (seen in the wild). */
  .key-card .key-swatch, .key-swatch {
    display: inline-block;
    vertical-align: -0.14em;
  }
  .key-entry + .key-entry { margin-top: var(--sp-1); }
  /* THE PROSE STANDS OFF ITS KEY. The paragraph states the track; the entries
     decode its marks. Run together they read as one wall.
     It lives HERE, after `.key-entry { margin: 0 }`, not up with the other
     tracks rules: both are one class, so the later of the two wins and the
     gap computed to 0 wherever it was written first. */
  .tracks-text + .key-entry { margin-top: var(--sp-4); }
  /* The gloss: what the mark MEASURES, under the word that names it. Set a
     stratum down and on its own line, so the list still scans by its words
     and the explanation is there for the reader who stops. `text-indent`
     resets because the entry hangs its wraps under the word, and the gloss
     is already there — it must not pull left into the swatch column. */
  .key-gloss {
    display: block;
    text-indent: 0;
    font-size: var(--micro);
    /* --label, NOT --ghost: quiet text is still text, and --ghost is a
       hairline colour that composites to 1.43:1. The italic is what carries
       "subordinate", per the token's own note. */
    font-style: italic;
    color: var(--label);
    line-height: 1.45;
  }
  .key-entry:has(.key-gloss) + .key-entry { margin-top: var(--sp-2); }
  .key-mark { color: var(--ink); }
  .key-italic { font-style: italic; }
  /* Four radars in a row — the ROW is the figure; a radar of one says
     nothing. Fixed at four columns, not responsive: nothing else on the
     site rewrites its own layout as the window moves. */
  .census-row {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: var(--sp-1);
    align-items: start;
  }
  /* A cell with a click is a CHOICE, so it follows the chant rows' law:
     the ones not taken sit at --label, hover brings one to ink, and the
     subject — not a choice, already open — stands at full ink unbuttoned. */
  .census-row button {
    display: block;
    width: 100%;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-align: inherit;
    cursor: pointer;
  }
  .census-row button:focus-visible {
    outline: var(--focus);
    outline-offset: var(--focus-out);
  }
  .census-who {
    font-family: var(--serif);
    font-size: var(--caption);
    font-style: italic;
    text-align: center;
    line-height: 1.25;
    margin: 2px 0 0;
    color: var(--label);
  }
  .census-row .subject .census-who { color: var(--ink); }
  .census-row button:hover .census-who { color: var(--ink); }
  .census-meta {
    font-family: var(--mono);
    font-size: var(--micro);
    color: var(--ghost);
    text-align: center;
    margin: 0;
  }

  /* ── the drawn figures ── */
  .panel svg { max-width: 100%; height: auto; display: block; }
  /* A wheel is a wheel: both wear the same frame, so both take the same room
     whatever sits in the panel beneath them. Left to the grid, the one with
     two tables under it stretches and the pair stop being comparable. */
  .panel svg.annulus, .panel svg.rota, .tabpanel svg.mutatio
    { width: 100%; max-width: 520px; aspect-ratio: 1; margin: 0 auto var(--sp-4); }
  /* The diagrams scale to their column by viewBox; without this they draw at
     their intrinsic width and overflow a narrow cell. Descendant, not child:
     every figure now sits inside its titled section. */
  .tabpanel svg { display: block; max-width: 100%; height: auto; }
  /* The monochord fills its column. It is a LINE — a string between two
     bridges — so width is the resolution it is read at: every extra pixel
     spreads the degrees further apart and makes the drift between the two
     measures easier to see. The round figures are capped instead, since a
     circle gains nothing past a certain size. */
  .tabpanel svg.chorda-dual { width: 100%; }
  /* The temper wheel is capped AT ITS OWN DRAWN SIZE: its viewBox is 440
     wide, so 440px renders every stroke and letter at the exact rung the
     ink system names. Uncapped it stretched to the column — ~18% over
     scale — and read boosted beside the other figures (his catch,
     2026-08-12: "it feels much more boosted big"). */
  .tabpanel svg.temper-wheel { max-width: 440px; margin: 0 auto var(--sp-4); }
  /* The hand standing ALONE — no chant, so no ring around it. Capped, because
     past a certain size a picture only gets bigger. */
  .tabpanel svg.hand {
    width: 74%;
    min-width: 220px;
    max-width: 380px;
    margin: 0 auto var(--sp-4);
  }
}
