/*
 * Shared stylesheet for the project site: the hand-written landing page and
 * every page build-site.js renders out of Markdown.
 *
 * Both typefaces are served from this origin. A Google Fonts <link> would
 * have been one line, but it also tells a third party who reads the page of
 * a tool whose whole point is that its output fetches nothing at run time.
 *
 * Inter Tight is copied out of node_modules at build time (variable weight
 * axis, latin subset). Iosevka is checked in under fonts/, subset to the
 * characters this site actually sets: the published latin cut is 961 KB
 * because it carries Iosevka's full variant and ligature inventory, and
 * nine tenths of that never reaches a code snippet on a web page.
 */

@font-face {
  font-family: 'Inter Tight Var';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url('fonts/inter-tight-latin-wght-normal.woff2') format('woff2-variations');
}
@font-face {
  font-family: 'Inter Tight Var';
  font-style: italic;
  font-display: swap;
  font-weight: 100 900;
  src: url('fonts/inter-tight-latin-wght-italic.woff2') format('woff2-variations');
}
@font-face {
  font-family: 'Iosevka Web';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url('fonts/iosevka-subset-400.woff2') format('woff2');
}

/*
 * Inter Tight: SIL OFL 1.1, Copyright The Inter Project Authors.
 * Iosevka: SIL OFL 1.1, Copyright Renzhi Li (aka. Belleve Invis).
 * Full licence texts: fonts/iosevka-OFL.txt and the packages they ship in.
 */

/*
 * Colours. The soft ink is the one that matters for accessibility: it sets
 * every caption, every window bar and the footer. At the lightness it used
 * to have (0.48) it measured 6.2:1 against the paper and 5.3:1 against the
 * window bar – so it passed WCAG 1.4.3, and the complaint about it was
 * real anyway, because it carried that contrast at 0.72rem. The sizes moved
 * up (see the type below) and this moved down with them, so the smallest
 * text on the page now has headroom rather than a bare pass.
 *
 * Every pair below is measured, not eyeballed; the lowest in either scheme
 * is 5.1:1.
 */
:root {
  --paper: oklch(0.98 0.00 0);
  --ink: oklch(0.20 0.01 260);
  --ink-soft: oklch(0.43 0.01 260);
  --accent: oklch(0.42 0.13 25);
  --rule: oklch(0.85 0.01 260);
  --frame: oklch(0.92 0.01 260);
  --shot-bg: oklch(0.95 0.005 260);
  --code-bg: oklch(0.945 0.005 260);
  /* Syntax tokens for the Markdown sample. Each is picked for >= 4.5:1 on
     --code-bg, not for looking like anyone's editor theme. */
  --tok-head: oklch(0.42 0.14 25);
  --tok-dir: oklch(0.44 0.14 265);
  --tok-code: oklch(0.40 0.09 165);
  /* The dimmed token still sets the `{.wide #id}` attribute tail, which is
     content a reader has to be able to read, not decoration. */
  --tok-punct: oklch(0.50 0.02 260);
  --sans: 'Inter Tight Var', 'Inter Tight', system-ui, -apple-system, sans-serif;
  --mono: 'Iosevka Web', ui-monospace, SFMono-Regular, Menlo, monospace;
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper: oklch(0.18 0.01 260);
    --ink: oklch(0.94 0.01 260);
    --ink-soft: oklch(0.74 0.01 260);
    --accent: oklch(0.78 0.12 25);
    --rule: oklch(0.36 0.01 260);
    --frame: oklch(0.28 0.01 260);
    --shot-bg: oklch(0.24 0.01 260);
    --code-bg: oklch(0.23 0.01 260);
    --tok-head: oklch(0.80 0.12 25);
    --tok-dir: oklch(0.80 0.11 265);
    --tok-code: oklch(0.82 0.10 165);
    --tok-punct: oklch(0.64 0.02 260);
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  /* Inter Tight is a condensed grotesque: at a given size it sets lighter
     and narrower than the serif this replaced, so it needs both a step up
     and a little more weight to read as solid rather than as fine print. */
  font-size: 21px;
  font-weight: 430;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
}

/*
 * Bands. The page is a single 8000px column, and on one flat background it
 * reads as one undifferentiated wall: nothing tells the eye that a section
 * has ended, so the whole length is present at once and the page looks like
 * more work than it is.
 *
 * The tint is not alternated. It marks what a section IS: a tinted band is
 * one you read (the argument, the invitation, the closing), a paper band is
 * one you look at or work through (the screenshots, the commands). That
 * happens to alternate here, which is luck, not the rule - if a section
 * changes character the tint follows the character.
 *
 * Screenshots and code stay on paper for a reason beyond rhythm: their own
 * chrome is `--shot-bg` and `--code-bg`, both within 0.035 lightness of the
 * band, so a tinted band would dissolve the very frames that make a
 * screenshot read as a window. Where a card cannot be moved out of a tinted
 * band it inverts to paper instead (see `.band-read .hint`).
 *
 * `main` no longer holds the measure, because a band has to reach both
 * edges of the viewport while its text stays at 48rem. The measure moved to
 * `.wrap`, and the breakout rule below moved with it.
 */
main { max-width: 48rem; margin: 0 auto; padding: 3.5rem 1.5rem 6rem; }
/* Only the two landing pages are banded. The documentation pages come out of
   build-site.js's shell, which has no bands and no wrap, and they keep the
   measure on `main` exactly as before. */
main.banded { max-width: none; padding: 0; }
.wrap { max-width: 48rem; margin: 0 auto; padding: 0 1.5rem; }
.band { padding: 4.5rem 0; }
.band-read { background: var(--shot-bg); }
/* Named rather than left to the default, so the two kinds of band are a pair
   in the stylesheet the way they are a pair on the page. */
.band-do { background: var(--paper); }
/* Tinted band, tinted card: the hint would sit 0.005 lightness from its own
   ground and disappear. Lifted to paper, it reads as a card either way. */
.band-read .hint { background: var(--paper); }
/* The hero carries its own opening space, and the university bar is directly
   above it. */
.band-hero { padding-top: 1.5rem; }
.band-close { padding-bottom: 3rem; }

/*
 * One visible focus ring for everything. The screenshots became buttons for
 * the lightbox and the hero bar carries a two-state switch, so keyboard
 * operability is not hypothetical here.
 */
:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

/*
 * The university bar, shared with psi-briefing so the chair's two tools frame
 * their pages the same way. Sticky, because it is the only affiliation the
 * page carries and a reader who has scrolled past the hero should still be
 * able to see whose work this is.
 *
 * It is deliberately NOT aligned to the reading column: the bar frames the
 * page rather than belonging to it, and running flush to both edges makes
 * that read as a frame instead of as a failed alignment.
 *
 * Its own colours, not the page tokens. The bar is the same dark grey in
 * light and dark mode – it is a constant, and a strip that changes with the
 * scheme reads as part of the content.
 */
.topbar {
  position: sticky;
  top: 0;
  z-index: 60;
  background: #3d3c3b;
  color: #ffffff;
  font-family: var(--sans);
}
.topbar-in { padding: 0.55rem 1.5rem; }
.topbar-row { display: flex; align-items: center; gap: 1.1rem; }
.topbar a { color: #ffffff; text-decoration: none; }
.topbar a:hover { text-decoration: underline; }
.topbar-brand {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.82rem;
  white-space: nowrap;   /* the wordmark is the one thing that must not wrap */
}
.topbar-brand svg { height: 21px; width: 21px; flex: none; }
/* Affiliation, then chair, then person: widest scope first, left to right. */
.topbar-right {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.78rem;
  min-width: 0;
  white-space: nowrap;
  /* min-width:0 lets this shrink past its content, and nowrap text does not
     wrap when it does – so without a clip it overflows the box and prints
     straight through the nav links to its right. The breakpoints below keep
     that from happening at any width this page is read at; these two lines
     are the backstop for the ones nobody measured, and they degrade to a
     truncated name rather than to two strings on top of each other. */
  overflow: hidden;
}
.topbar-chair { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.topbar-right a { color: #d6d4d2; }
.topbar-right a:hover { color: #ffffff; }
.topbar-sep { color: #96938f; }
/* Everything the reader navigates with, held at the right end. The auto
   margin lives here, so dropping the links at narrow widths cannot pull the
   rest back into the middle of the row. */
.topbar-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 1.1rem;
  font-size: 0.78rem;
  white-space: nowrap;
}
.topbar-nav { display: none; }
@media (min-width: 60rem) {
  .topbar-nav { display: flex; gap: 1.1rem; font-size: 0.78rem; white-space: nowrap; }
}

/*
 * The language switch. Two links, always both visible, the current one marked
 * with aria-current. Not a select and not a redirect: a reader who lands on
 * one language can see at a glance that the other exists and what it is
 * called, and the URL they end up on is the one they can share.
 */
.topbar-lang { display: flex; align-items: center; gap: 0.15rem; font-size: 0.74rem; }
.topbar-lang a {
  padding: 0.15rem 0.35rem;
  border-radius: 3px;
  color: #d6d4d2;
  letter-spacing: 0.03em;
}
.topbar-lang a:hover { color: #ffffff; text-decoration: none; background: #4d4c4a; }
.topbar-lang a[aria-current] { color: #2a2a2e; background: #ffffff; }


/*
 * The narrow-width menu. A <details> rather than a scripted dropdown: it is
 * the one native control that already does keyboard operation and the right
 * semantics, and its contents stay out of the tab order while it is closed.
 * site.js only adds what the element does not do by itself – closing on
 * Escape and on a click outside – so the menu works with scripting off.
 *
 * It carries the affiliation links too. Those are dropped from the bar below
 * 56rem and 40rem, and on a phone the menu would otherwise be the only place
 * they could live, which is to say nowhere.
 */
.topbar-menu { position: relative; flex: none; }
.topbar-menu > summary {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.25rem 0.45rem;
  font-size: 0.78rem;
  color: #ffffff;
  cursor: pointer;
  list-style: none;              /* Firefox draws a marker without this */
  border: 1px solid transparent;
  border-radius: 4px;
}
.topbar-menu > summary::-webkit-details-marker { display: none; }
.topbar-menu > summary:hover,
.topbar-menu[open] > summary { border-color: #86827f; }
.topbar-menu > summary:focus-visible { outline: 2px solid #ffffff; outline-offset: 1px; }
.topbar-menu svg { width: 14px; height: 12px; flex: none; }
.topbar-panel {
  position: absolute;
  top: calc(100% + 0.4rem);
  right: 0;
  /* Viewport-bounded on both sides: a bare min-width beats max-width in the
     cascade, and at 320px the panel would hang off the left edge. */
  min-width: min(17rem, calc(100vw - 2rem));
  max-width: min(22rem, calc(100vw - 2rem));
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--rule);
  border-radius: 5px;
  padding: 0.4rem 0;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.22);
}
.topbar-panel a {
  display: block;
  padding: 0.5rem 0.9rem;
  color: var(--ink);
  font-size: 0.92rem;
  line-height: 1.35;
}
.topbar-panel a:hover { background: var(--shot-bg); text-decoration: none; }
.topbar-panel hr { border: 0; border-top: 1px solid var(--rule); margin: 0.4rem 0; }
/* Above the breakpoint the row carries the links itself. */
@media (min-width: 60rem) {
  .topbar-menu { display: none; }
}
/* The bare GitHub link is what the menu replaces, so it goes when the menu
   arrives – two ways to the same place in a strip this size is clutter. */
@media (max-width: 60rem) {
  .topbar-gh { display: none; }
}
/* Dropped in stages: the chair's name is the longest string in the bar and
   the first thing that stops fitting; the person is the attribution that
   matters, so it goes last. A sticky strip must never grow a second line. */
@media (max-width: 56rem) {
  .topbar-chair, .topbar-sep { display: none; }
}
/* The same drop again, one band higher. Above 60rem the row grows the section
   links, and from there the whole bar needs 1050px in English and 1075px in
   German – measured, with the chair name alone accounting for 354px of the
   German figure. Between the two widths the chair name is what does not fit,
   so it goes here too and comes back at 68rem. Without this the row does not
   wrap and does not clip: the nowrap text simply runs through the nav links. */
@media (min-width: 60rem) and (max-width: 68rem) {
  .topbar-chair, .topbar-sep { display: none; }
}
@media (max-width: 40rem) {
  .topbar-right { display: none; }
  .topbar-in { padding: 0.5rem 1rem; }
}
/* Anchored headings have to clear the bar, or the browser parks them under it. */
h2[id], h3[id] { scroll-margin-top: 4rem; }

h1 { font-size: 2.6rem; line-height: 1.08; margin: 0 0 0.5rem; letter-spacing: -0.02em; }
.tagline, .lead { color: var(--ink-soft); margin: 0 0 3rem; font-size: 1.1rem; }

/*
 * The hero. The first draft opened with the wordmark and then a wall of
 * Markdown, which announced "this is a technical thing" before it said what
 * the thing was good for. Now the promise is the headline and the proof is
 * two pictures directly under it: the same paragraph as the room sees it and
 * as the reader gets it. That is the whole idea, and it does not need a
 * paragraph to explain it.
 */
.hero { padding: 1rem 0 0; }
.hero .mark {
  font-family: var(--mono);
  font-size: 0.95rem;
  color: var(--ink-soft);
  letter-spacing: 0.02em;
  margin: 0 0 1.2rem;
}
.hero .mark b { color: var(--ink); font-weight: 400; }
/* What the thing *is*, said before the headline says what it is for. The
   headline alone ("Write the lecture once") reads as a writing method; a
   visitor should not have to infer from the screenshots that this is
   presentation software. */
.hero .what {
  font-size: 1.05rem;
  color: var(--ink);
  margin: 0 0 0.9rem;
  max-width: 34rem;
  font-weight: 500;
}
/* The headline runs the full width of the hero, not a 20ch measure. The cap
   was set when the hero was one narrow column; against the two columns and
   the two screenshots it now sits over, it broke the German headline into a
   second line while two thirds of the row stood empty beside it. */
.hero h1 { font-size: 3.1rem; margin-bottom: 0.9rem; text-wrap: balance; }
/*
 * The hero head sits on the same width and the same left edge as the pictures
 * under it, and its prose runs in the same two columns. Stacked, it was tall
 * enough that on a 1080p screen the pair below never came into view, which
 * cost the page the one thing it is trying to show. Two columns halve that
 * height; matching `.pair`'s grid exactly is what makes the split read as
 * composition rather than as a paragraph that happened to wrap.
 *
 * It also settles a second left edge. The prose used to start at main's 48rem
 * measure while the pictures broke out to 66rem, so the hero began 7.5rem to
 * the right of its own screenshots.
 *
 * There is one size of body text in the hero, and that is deliberate. The
 * promise sentence used to be set at 1.22rem, larger than the paragraph above
 * it, so a reader scanning by size met it first and met "all four" before
 * anything had said four of what. Read in a column it needs no size to be
 * found, and it has since folded back into the paragraph it belongs to.
 */
.hero-in { width: min(94vw, 66rem); margin-left: 50%; transform: translateX(-50%); }
.hero-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 1.2rem; }
.hero-col { display: flex; flex-direction: column; }
/* Buttons to the foot of the column, so the two columns end on one line
   however the prose falls. */
.hero-col .cta { margin-top: auto; margin-bottom: 2rem; }
@media (max-width: 46rem) {
  .hero-cols { grid-template-columns: 1fr; }
  .hero-col .cta { margin-top: 0.6rem; }
  /* Stacked, there is no second column to line up with, and the breakout
     would put the hero 12px from the edge while the article below it sits at
     24px. Back into the normal flow, so the phone gets one left edge too. */
  .hero-in { width: auto; margin-left: 0; transform: none; }
}
.hero .extras {
  font-size: 1rem;
  color: var(--ink-soft);
  max-width: 34rem;
  margin: 0 0 1.8rem;
}
.hero .extras b { color: var(--ink); font-weight: 600; }

.cta { display: flex; flex-wrap: wrap; gap: 0.7rem; margin: 0 0 2.4rem; }
.cta a {
  display: inline-block;
  font-size: 1rem;
  font-weight: 550;
  text-decoration: none;
  padding: 0.55rem 1.1rem;
  border-radius: 6px;
  border: 1px solid var(--ink);
  min-height: 2.6rem;
}
.cta a.primary { background: var(--ink); color: var(--paper); }
.cta a.secondary { color: var(--ink); }
.cta a:hover { border-color: var(--accent); }
/* `--paper` rather than a literal white: on the dark scheme the accent is a
   light rose, and white-on-rose is the one pairing that fails there. */
.cta a.primary:hover { background: var(--accent); border-color: var(--accent); color: var(--paper); }
.cta a.secondary:hover { color: var(--accent); }
/* The hero pair breaks out of the reading measure (see the shared rule
   further down): side by side inside a 48rem column each shot lands at a
   third of its captured size, and the contrast the pair exists to show –
   four short lines against four full paragraphs – stops being legible as
   text and reads as two grey blocks. */
.hero .pair { gap: 1.4rem; margin-bottom: 0; }
.hero .shot-note {
  font-size: 1rem;
  color: var(--ink-soft);
  text-align: center;
  max-width: 52rem;
  margin: 0.2rem auto 3.4rem;
}
.hero .shot-note b { color: var(--ink); font-weight: 600; }
@media (max-width: 46rem) {
  .hero h1 { font-size: 2.3rem; }
}

/*
 * Where the script ends up.
 *
 * The section makes an argument in three moves: a lecture holds three kinds
 * of text, the usual tools have room for two, and here is what that costs.
 * The diagram is the second and third moves, drawn against the same three
 * columns so the rows can be compared straight down: the slide, the handout,
 * the notes.
 *
 * Every row is one file glyph, the run of wire leaving it, and what arrives.
 * A dashed footprint is a kind of text with nowhere to go - the same shape as
 * the thing that is missing, so the gap is legible as a gap. Row one pours
 * the script onto the slide and the slide fills edge to edge, which is the
 * paragraph's "death by PowerPoint" without the phrase. Row two runs two
 * files on two wires that never meet, and the break drawn between them is
 * the drift the paragraph describes. Row three is one wire reaching all
 * three.
 *
 * It carries no instructions and nothing to operate. An earlier version let
 * you lift the sheets and had to say so in a caption, which is the tell that
 * a picture has nothing to say: the reader was being given a mechanic in
 * place of an argument.
 */
.kinds {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.02fr);
  gap: 3rem;
  align-items: center;
  width: min(94vw, 66rem);
  margin-left: 50%;
  transform: translateX(-50%);
}
.kinds-prose > p:first-child { margin-top: 0; }
.kinds-figure { min-width: 0; }
/* The heading belongs to the two columns under it, so it breaks out with
   them. Left at the 48rem measure it began 144px inboard of its own figure,
   the same two-left-edge fault the hero had. */
.band-kinds > .wrap > h2 { width: min(94vw, 66rem); margin-left: 50%; transform: translateX(-50%); }
@media (max-width: 56rem) {
  .kinds { grid-template-columns: 1fr; gap: 2.4rem; width: auto; margin-left: 0; transform: none; }
  .band-kinds > .wrap > h2 { width: auto; margin-left: 0; transform: none; }
}

.paths { display: flex; flex-direction: column; gap: 1.5rem; }
.paths > svg { display: block; width: 100%; height: auto; }
.path { margin: 0; }
.path svg { display: block; width: 100%; height: auto; }
.path svg text { font-family: var(--mono); }
.path figcaption {
  font-family: var(--mono);
  font-size: 0.76rem;
  color: var(--ink-soft);
  margin: 0 0 0.3rem;
  padding: 0;
  max-width: none;
}
/* The row that resolves the other two says so in the accent, the same way a
   topic sentence does everywhere else on the page. */
.path-fix figcaption { color: var(--accent); }

/*
 * Tying the rows to the sentences they draw.
 *
 * This earns its place only because both halves now make the same argument.
 * Pointing at "the slides take its job and fill up" raises the row where the
 * slide is full; pointing at that row raises the sentence. The pair is the
 * same claim in two notations, and the link is the thing a reader would
 * otherwise have to assemble.
 *
 * Emphasis is by recession: the two rows that are not being asked about drop
 * back, which reads as one row coming forward without anything having to
 * move. The picked row also lifts to paper, the same inversion `.hint` uses
 * inside a tinted band.
 *
 * Pointer only, and deliberately so. Everything is legible and complete at
 * rest, the sentences stay sentences rather than becoming three tab stops
 * inside a paragraph, and nothing here is the only way to reach anything.
 */
/* The clause has to look connected to something before anyone points at it.
   Dotted rather than solid: over three lines of prose a solid rule reads as
   an underlined sentence, which is emphasis, and these are not emphasised -
   they are attached. Dotted goes solid and accent when the pair lights up,
   so the resting state and the active state are the same mark twice. */
.tie {
  cursor: pointer;
  text-decoration: underline dotted var(--ink-soft);
  text-decoration-thickness: 1.5px;
  text-underline-offset: 0.24em;
  transition: color 160ms ease, text-decoration-color 160ms ease;
}
.tie:hover,
.tie.is-tied {
  color: var(--accent);
  text-decoration: underline solid var(--accent);
  text-decoration-thickness: 1.5px;
  text-underline-offset: 0.24em;
}
.path { cursor: pointer; transition: opacity 200ms ease, background-color 200ms ease; }
/* Reserved at all times, so nothing shifts when the ground appears. */
.path { padding: 0.5rem 0.6rem; border-radius: 8px; margin: -0.5rem -0.6rem; }
.paths.is-tied .path { opacity: 0.3; }
.paths.is-tied .path.is-tied { opacity: 1; background: var(--paper); }

@media (prefers-reduced-motion: reduce) {
  .tie, .path { transition: none; }
}

/*
 * The walkthrough for a machine with nothing on it.
 *
 * Closed by default, because opened it is longer than the section around it
 * and a reader who already has a terminal would scroll past all of it to
 * reach the next heading. A <details> rather than a script: the native marker
 * already rotates, answers to the keyboard and prints open.
 *
 * The steps are numbered by CSS counter, and the numbers are information
 * rather than ornament - this is a sequence where doing step four before
 * step two leaves you in the wrong folder.
 *
 * It sits in a paper band, so the box is paper with a rule around it and the
 * code blocks inside stay tinted. Tinting the box itself would put it within
 * 0.005 lightness of its own `pre` blocks, the same collision the bands had
 * to avoid.
 */
.setup {
  border: 1px solid var(--rule);
  border-radius: 6px;
  margin: 1.6rem 0 2rem;
}
.setup > summary {
  padding: 0.75rem 1.1rem;
  cursor: pointer;
  font-size: 0.98rem;
  font-weight: 600;
}
.setup[open] > summary { border-bottom: 1px solid var(--rule); }
.setup-body { padding: 1.2rem 1.1rem 1.5rem; counter-reset: step; }
/* The first heading already carries a top margin; the padding above it would
   otherwise push the panel away from the summary it belongs to. */
.setup-body > :first-child { margin-top: 0; }
.setup-body h3 {
  counter-increment: step;
  font-size: 1.05rem;
  margin: 1.9rem 0 0.5rem;
}
.setup-body h3::before { content: counter(step) ". "; color: var(--ink-soft); }
.setup-body p { max-width: none; }
.setup-body pre { font-size: 0.88rem; padding: 0.7rem 0.9rem; }

/*
 * Section headings carry the page. The first draft set them as small grey
 * uppercase eyebrows, which left the screenshots and the running text at
 * roughly the same visual weight and made the page hard to scan.
 */
h2 {
  font-size: 1.6rem;
  font-weight: 650;
  letter-spacing: -0.015em;
  color: var(--ink);
  margin: 0 0 1.2rem;
}
/* The rule draws a boundary the band has not already drawn. A heading that
   opens its band has the band edge above it and needs no second line under
   the first; a heading that follows something inside one band is the only
   place left where a section starts without the ground changing colour, and
   there it does the whole job. */
.band > .wrap > h2 ~ h2 {
  margin-top: 4rem;
  padding-top: 1.1rem;
  border-top: 2px solid var(--ink);
}
h3 { font-size: 1.15rem; font-weight: 650; margin: 2.2rem 0 0.5rem; }

a { color: var(--accent); }
p, li { max-width: 40rem; }

/* Inline code has no spaces to break at, so a path or a winget invocation in
   running prose pushes the page sideways on a phone. It breaks only when it
   would otherwise overflow, which is the trade worth making: a command split
   across two lines is readable, a page that scrolls horizontally is not.
   Blocks are exempt - `pre` scrolls inside its own box, and breaking a
   transcript would misquote it. */
code { font-family: var(--mono); font-size: 0.92em; overflow-wrap: break-word; }
pre code { overflow-wrap: normal; }
pre {
  font-family: var(--mono);
  /* Iosevka is narrow, so it can carry a longer line than a normal mono at
     the same size – but it also reads smaller, which is why this sits above
     the 0.9rem that looked right in a specimen and read as fine print on the
     page. A Markdown sample is meant to be read, not squinted at. */
  font-size: 0.96rem;
  line-height: 1.6;
  background: var(--code-bg);
  border: 1px solid var(--rule);
  padding: 1rem 1.1rem;
  border-radius: 4px;
  overflow-x: auto;
}
pre code { font-size: inherit; }

/*
 * Tokens for the Markdown sample on the landing page. Marked up by hand in
 * index.html rather than highlighted at run time: it is one snippet, a
 * Markdown-with-psi-directives grammar has no off-the-shelf highlighter that
 * knows what `::: cols 2` is, and a regex tokeniser in site.js would be more
 * code than the snippet it colours.
 */
pre .t-head   { color: var(--tok-head); font-weight: 600; }
pre .t-dir    { color: var(--tok-dir); }
pre .t-code   { color: var(--tok-code); }
pre .t-strong { font-weight: 700; }
pre .t-note   { color: var(--ink-soft); font-style: italic; }
pre .t-cmt    { color: var(--ink-soft); }
pre .t-punct  { color: var(--tok-punct); }

/*
 * The shell blocks reuse those same variables rather than introducing a
 * second palette: the command carries the heading colour because it is the
 * thing you are meant to read first, options take the directive blue, and
 * paths and URLs take the colour inline code already has. Adding a real
 * highlighter here would mean a stock editor theme whose colours are chosen
 * to look like an editor, not to clear 4.5:1 on this background.
 */
pre .t-cmd    { color: var(--tok-head); font-weight: 600; }
pre .t-flag   { color: var(--tok-dir); }
pre .t-path   { color: var(--tok-code); }

kbd {
  font-family: var(--mono);
  font-size: 0.85em;
  border: 1px solid var(--rule);
  border-bottom-width: 2px;
  border-radius: 4px;
  padding: 0.05em 0.35em;
  background: var(--paper);
}

blockquote {
  margin: 1.4rem 0;
  padding-left: 1.1rem;
  border-left: 2px solid var(--rule);
  color: var(--ink-soft);
}

table {
  border-collapse: collapse;
  width: 100%;
  font-size: 0.95rem;
  display: block;
  overflow-x: auto;
}
th, td { border-bottom: 1px solid var(--rule); padding: 0.5rem 0.6rem; text-align: left; vertical-align: top; }
th { font-weight: 650; color: var(--ink-soft); }

hr { border: 0; border-top: 1px solid var(--rule); margin: 2.6rem 0; }
/* A Markdown file that separates its sections with --- would otherwise draw
   a thin rule directly above the heading's heavy one. */
hr + h2 { border-top: 0; padding-top: 0; margin-top: 1.6rem; }

/*
 * Screenshots. The point of the frame is that a reader can tell at a glance
 * which pixels are the tool and which are this page: without it the shots
 * read as more page, and the layout stops parsing.
 */
figure.shot {
  /* Tight under the picture, generous under the caption: a caption that sits
     equidistant between its own image and the next one attaches itself to
     whichever the eye reaches first, which on a scrolling page is the wrong
     one. Same reason `pre + p.tiny` is pulled up below. */
  margin: 0 0 3.2rem;
  border: 1px solid var(--frame);
  border-radius: 7px;
  overflow: hidden;
  background: var(--shot-bg);
}
figure.shot .bar {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.35rem 0.75rem;
  /* The switch is taller than a line of the bar's own text, so without a
     floor the two shots of a pair sit at different heights and the row
     reads as a mistake. */
  min-height: 2.55rem;
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--ink-soft);
  background: var(--frame);
  border-bottom: 1px solid var(--rule);
}
figure.shot .bar b { font-weight: 400; color: var(--ink); }
figure.shot .dots { display: flex; gap: 0.3rem; }
figure.shot .dots i {
  width: 0.56rem; height: 0.56rem; border-radius: 50%;
  background: var(--rule); display: block;
}
figure.shot img {
  width: 100%; height: auto; display: block; background: var(--paper);
  cursor: zoom-in;
}
figure.shot img:focus-visible { outline: 3px solid var(--accent); outline-offset: -3px; }

/*
 * The film sits in the same frame as the screenshots, for the same reason:
 * it is a picture of the tool, not more page. It is a plain <video> with the
 * browser's own controls and no player library – the file streams from the
 * chair's server, so the page fetches nothing until someone presses play,
 * and the poster underneath is a local WebP so the box has its height from
 * the first paint and the layout never jumps.
 *
 * `aspect-ratio` rather than a height: the poster is 16:9 and so is the
 * clip, and stating it keeps the frame the right shape even in the moment
 * before the poster has decoded.
 */
figure.shot video {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  display: block;
  background: var(--paper);
}

/*
 * The bar doubles as a two-state switch on the hero's right-hand shot. It
 * used to be two words in the bar's own grey, distinguishable from the file
 * name beside them only by a faint background on the active one – so nobody
 * pressed it. It is a segmented control now: its own framed group, a spoken
 * label, and an active segment in full ink.
 */
figure.shot .swap {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  margin-left: auto;
  padding: 0.15rem;
  border: 1px solid var(--ink-soft);
  border-radius: 6px;
  background: var(--paper);
}
figure.shot .swap .swap-label {
  font-size: 0.8rem;
  color: var(--ink-soft);
  padding-left: 0.35rem;
}
figure.shot .swap button {
  font-family: var(--mono);
  font-size: 0.85rem;
  line-height: 1.5;
  border: 1px solid transparent;
  background: none;
  color: var(--ink);
  /* >= 24 px tall including the border, for WCAG 2.5.8 target size. */
  padding: 0.2rem 0.5rem;
  border-radius: 4px;
  cursor: pointer;
}
figure.shot .swap button:hover { border-color: var(--accent); color: var(--accent); }
figure.shot .swap button[aria-pressed="true"] {
  background: var(--ink);
  color: var(--paper);
  border-color: var(--ink);
}
figure.shot .swap button[aria-pressed="true"]:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--paper);
}
@media (max-width: 34rem) {
  figure.shot .bar { flex-wrap: wrap; }
  figure.shot .swap { margin-left: 0; }
}

/*
 * Lightbox. `hidden` alone loses to the display rule, so it is restated.
 */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(18, 18, 22, 0.9);
  display: grid;
  place-items: center;
  padding: 2.5vmin;
  cursor: zoom-out;
}
.lightbox[hidden] { display: none; }
.lightbox img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  border-radius: 4px;
  background: var(--paper);
  box-shadow: 0 16px 60px rgba(0, 0, 0, 0.55);
}
body.lightbox-open { overflow: hidden; }
figure.shot figcaption {
  font-size: 1rem;
  color: var(--ink-soft);
  padding: 0.6rem 0.9rem 0.9rem;
  max-width: 48rem;
}
figure.shot figcaption strong { color: var(--ink); font-weight: 650; }

.pair { display: grid; gap: 1.2rem; grid-template-columns: 1fr 1fr; margin-bottom: 3.2rem; }
.pair figure.shot { margin-bottom: 0; }
@media (max-width: 46rem) { .pair { grid-template-columns: 1fr; } }

/*
 * Screenshots break out of the reading measure. Prose wants 40rem or it
 * stops being readable; a screenshot wants every pixel it can get, because
 * the whole claim being made is that the projection is legible and the
 * cockpit is a workspace, and neither survives being shrunk to a third.
 * The two are set independently: the column stays narrow, the pictures
 * bleed into the margin on both sides.
 */
.wrap > figure.shot,
.wrap > .pair,
.hero .pair {
  width: min(94vw, 66rem);
  margin-left: 50%;
  transform: translateX(-50%);
}

.hint {
  background: var(--shot-bg);
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 1rem 1.2rem;
  font-size: 1rem;
  margin: 2rem 0 0;
}
.hint p { margin: 0; max-width: none; }

p.tiny { font-size: 0.95rem; color: var(--ink-soft); }
/* A line that explains the block above it is a caption, so it sits close to
   what it captions and keeps its distance from what follows. */
pre + p.tiny { margin: 0.55rem 0 3rem; }

footer {
  margin-top: 4rem;
  padding-top: 1.2rem;
  border-top: 1px solid var(--rule);
  font-size: 0.95rem;
  color: var(--ink-soft);
}
footer p { max-width: none; }
