/* Structure: layout, rhythm, and component anatomy. Color, type, texture,
   and ornament belong to garden.css, which layers over these neutral
   defaults and can be swapped wholesale without touching markup. */
@layer reset, tokens, base, layout, prose, components, garden;

@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }
  html {
    text-size-adjust: none;
    -webkit-text-size-adjust: none;
  }
  body,
  h1,
  h2,
  h3,
  h4,
  p,
  ul,
  ol,
  li,
  figure,
  blockquote,
  pre,
  hr {
    margin: 0;
  }
  img,
  picture,
  svg,
  video {
    display: block;
    max-inline-size: 100%;
    block-size: auto;
  }
  input,
  button,
  textarea,
  select {
    font: inherit;
  }
}

@layer tokens {
  :root {
    color-scheme: light dark;
    --paper: light-dark(#fafafa, #161616);
    --surface: light-dark(#f0f0f0, #222);
    --ink: light-dark(#222, #eee);
    --ink-soft: light-dark(#666, #aaa);
    --accent: light-dark(#0b57d0, #8ab4f8);
    --link: var(--accent);
    --rule: light-dark(#ddd, #333);
    --rule-strong: light-dark(#bbb, #555);
    --highlight: light-dark(#fff3a3, #4a4320);

    --font-prose: Georgia, serif;
    --font-mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;

    --measure: 40rem;
    --wide-extra: 8rem;
    --sidenote-gap: 2.5rem;
    --sidenote-width: 16rem;
    --pad-inline: clamp(1.25rem, 6vw, 7rem);
    --pad-block: clamp(1.25rem, 4vh, 2.75rem);
    --rail-width: clamp(15rem, 22vw, 21rem);
    --rail-slim: 4.5rem;
    --flow: 1.25em;
    --radius: 0.4rem;

  }
}

@layer base {
  html {
    background-color: var(--paper);
    color: var(--ink);
    font-family: var(--font-prose);
    font-size: clamp(1.0625rem, 0.98rem + 0.3vw, 1.1875rem);
    line-height: 1.65;
    font-synthesis: none;
    hanging-punctuation: first last;
  }
  body {
    min-block-size: 100dvh;
  }
  h1,
  h2,
  h3,
  h4 {
    line-height: 1.15;
    text-wrap: balance;
  }
  p,
  li,
  dd,
  figcaption {
    text-wrap: pretty;
  }
  a {
    color: var(--link);
  }
  :focus-visible {
    outline: 0.15rem solid var(--accent);
    outline-offset: 0.2rem;
    border-radius: 0.15rem;
  }
  code,
  kbd,
  samp,
  pre {
    font-family: var(--font-mono);
    font-size: 0.86em;
  }
  ::selection {
    background: var(--highlight);
    color: var(--ink);
  }
}

@layer layout {
  /* The shell: content on the left, a navigation rail on the right. On
     narrow screens the rail drops below the content. */
  .shell {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    min-block-size: 100dvh;
  }
  .column {
    display: flex;
    flex-direction: column;
    min-inline-size: 0;
    padding-block: var(--pad-block);
    padding-inline: var(--pad-inline);
  }
  .rail nav {
    display: flex;
    flex-direction: column;
    justify-content: end;
    padding-block: var(--pad-block);
    padding-inline: var(--pad-inline);
  }
  @media (min-width: 60rem) {
    .shell {
      grid-template-columns: minmax(0, 1fr) var(--rail-width);
    }
    .rail nav {
      position: sticky;
      inset-block-start: 0;
      block-size: 100dvh;
      padding-inline: clamp(1.5rem, 2.5vw, 2.5rem);
    }
  }
  /* Until 84rem the rail folds to its glyphs, so main reaches the 52rem
     that sidenotes need from about 1090px (see the sidenote container
     query). At 84rem the full rail still leaves main just over 52rem, so
     notes stay put as it unfolds: keep the two in step if --rail-width or
     the threshold changes. */
  @media (min-width: 60rem) and (max-width: 83.99rem) {
    .shell {
      grid-template-columns: minmax(0, 1fr) var(--rail-slim);
    }
    .rail nav {
      padding-inline: 0.75rem;
    }
  }

  /* Main is a named-line grid anchored left: prose sits on the text track,
     and wide content breaks out to the right through subgrid. */
  main {
    flex: 1;
    display: grid;
    grid-template-columns:
      [full-start text-start wide-start] min(var(--measure), 100%)
      [text-end] minmax(0, var(--wide-extra))
      [wide-end] minmax(0, 1fr)
      [full-end];
    align-content: start;
    padding-block-start: clamp(3rem, 10vh, 7rem);
    container: main / inline-size;
  }
  main > *,
  .article > *,
  .article-body > * {
    grid-column: text;
  }
  main > .article,
  .article > .article-body {
    grid-column: full;
    display: grid;
    grid-template-columns: subgrid;
  }
  .article-body > :is(pre, table, .plates, .plate[data-size="wide"]) {
    grid-column: wide;
  }
  .article-body > .plate[data-size="full"] {
    grid-column: full;
  }

  /* Landing: the intro sits a little lower than other pages' headers, in
     the open space below the breadcrumb row. */
  .landing {
    display: grid;
    align-content: start;
    gap: clamp(1.75rem, 5vh, 3rem);
    padding-block-start: clamp(0rem, 8vh, 6rem);
  }
}

@layer prose {
  .article-body > * + * {
    margin-block-start: var(--flow);
  }
  .article-body > :is(h2, h3, h4) {
    margin-block-start: 2.2em;
  }
  .article-body > :is(h2, h3, h4) + * {
    margin-block-start: 0.75em;
  }
  /* Grid items never collapse margins, so set-pieces space themselves
     from what precedes them and push away what follows. */
  .article-body > :where(hr, pre, table, .callout, blockquote, .plate, .plates),
  .article-body > :where(hr, pre, table, .callout, blockquote, .plate, .plates) + * {
    margin-block-start: 2em;
  }
  .article-body > :is(h2, h3, h4, *):first-child {
    margin-block-start: 0;
  }
  .article-body > blockquote + blockquote {
    margin-block-start: 0.75em;
  }
  .article-body :is(ul, ol) {
    padding-inline-start: 1.35em;
  }
  .article-body li + li,
  .article-body li > :is(ul, ol) {
    margin-block-start: 0.4em;
  }
  .article-body li > p + p {
    margin-block-start: 0.6em;
  }
  .article-body blockquote > * + * {
    margin-block-start: 0.75em;
  }
  /* Section permalinks hang in the left margin of their heading. */
  .article-body :is(h2, h3, h4) {
    position: relative;
    scroll-margin-block-start: 2rem;
  }
  .heading-anchor {
    position: absolute;
    inset-inline-end: 100%;
    inset-block-start: 0;
    padding-inline-end: 0.45em;
  }

  /* Plates: figures built from image-only paragraphs. */
  .article-body {
    counter-reset: plate;
  }
  .plate {
    counter-increment: plate;
    display: grid;
    gap: 0.6rem;
    justify-items: center;
  }
  .plate img {
    border-radius: var(--radius);
  }
  .plate[data-size="full"] img {
    inline-size: 100%;
    border-radius: 0;
  }
  .plate[data-pixel] img {
    image-rendering: pixelated;
  }
  .plate figcaption {
    justify-self: stretch;
  }
  .plates {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
    gap: 1rem;
    align-items: start;
  }
  .plates img {
    inline-size: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
  }

  pre {
    overflow-x: auto;
    padding: 1rem 1.25rem;
    line-height: 1.5;
    tab-size: 2;
  }
  pre code {
    font-size: inherit;
  }
  :not(pre) > code {
    padding: 0.1em 0.3em;
    border-radius: 0.25rem;
    overflow-wrap: anywhere;
  }

  table {
    display: block;
    overflow-x: auto;
    border-collapse: collapse;
    font-variant-numeric: tabular-nums;
  }
  th,
  td {
    padding: 0.5em 0.9em;
    text-align: start;
    vertical-align: top;
  }

  hr {
    border: 0;
    block-size: 1px;
    background: var(--rule);
  }
  /* The footnote section draws its own divider. */
  .article-body > hr:has(+ .footnotes) {
    display: none;
  }
  .footnotes {
    font-size: 0.88em;
  }
  .footnotes ol {
    padding-inline-start: 1.5em;
  }
  .footnotes li + li {
    margin-block-start: 0.6em;
  }

  /* Sidenotes: each footnote is also copied beside its first reference
     (src/content/footnotes.odin), and one of the two copies shows. Where
     main fits the measure, a gap, and a note, the copy hangs in the margin
     and the notes section is hidden; elsewhere the copy is hidden. */
  .sidenote {
    display: none;
  }
  /* A note may hang halfway into the column's padding, so 52rem leaves it
     about 12rem: --measure + --sidenote-gap + 12rem - --pad-inline / 2. */
  @container main (min-width: 52rem) {
    .article-body {
      position: relative;
    }
    /* No block offset, so the note keeps its static position: the line its
       reference sits on. That needs an inline display (position: absolute
       blockifies the box anyway); a block's static position is the next
       line. Absolute rather than floated, because a grid item contains its
       floats and a long note would push the next paragraph down. (A
       reference inside a heading, which is positioned for its permalink,
       would place its note against the heading instead.) */
    .sidenote {
      display: inline;
      position: absolute;
      inset-inline-start: calc(var(--measure) + var(--sidenote-gap));
      inline-size: min(
        var(--sidenote-width),
        100cqi - var(--measure) - var(--sidenote-gap) + var(--pad-inline) / 2
      );
    }
    /* The first paragraph runs on from the note's number. */
    .sidenote-para + .sidenote-para {
      display: block;
      margin-block-start: 0.5em;
    }
    .footnotes {
      display: none;
    }
  }
  sup {
    line-height: 0;
  }
}

@layer components {
  .skip-link {
    position: fixed;
    inset-block-start: 0.5rem;
    inset-inline-start: 0.5rem;
    z-index: 10;
    translate: 0 -200%;
    padding: 0.5rem 0.75rem;
    background: var(--ink);
    color: var(--paper);
  }
  .skip-link:focus {
    translate: 0;
  }

  .eyebrow,
  .callout-label {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    line-height: 1.4;
  }

  .page-header,
  .article-header {
    margin-block-end: clamp(2rem, 6vw, 3.5rem);
  }
  .page-header > * + *,
  .article-header > * + * {
    margin-block-start: 0.9rem;
  }

  .breadcrumb ol {
    display: flex;
    flex-wrap: wrap;
    padding: 0;
    list-style: none;
  }
  .breadcrumb li + li::before {
    content: "/";
    padding-inline: 0.75ch;
  }

  .rail-links {
    padding: 0;
    list-style: none;
  }
  .rail-links a {
    display: grid;
    grid-template-columns: 2.75rem 1fr;
    align-items: center;
    padding: 0.85rem 0.5rem;
  }
  /* The folded rail shows glyphs alone. Each label leaves the layout but
     still names its link, and slides out to the left of the rail on hover
     or focus (clipped, not hidden, so it stays in the accessibility tree). */
  @media (min-width: 60rem) and (max-width: 83.99rem) {
    .rail-links a {
      position: relative;
      grid-template-columns: 1fr;
      justify-items: center;
      padding-inline: 0;
    }
    .rail-label {
      position: absolute;
      inset-block: 0;
      inset-inline-end: 100%;
      display: flex;
      align-items: center;
      padding-inline: 0.85rem;
      white-space: nowrap;
      clip-path: inset(0 0 0 100%);
    }
    .rail-links a:is(:hover, :focus-visible) .rail-label {
      clip-path: inset(0);
    }
  }

  .latest {
    display: grid;
    gap: 0.35rem;
    justify-items: start;
    max-inline-size: 36rem;
  }

  .terminal {
    display: flex;
    flex-direction: column;
    justify-content: end;
    min-block-size: 0;
  }
  .terminal-output {
    flex: 0 1 auto;
    min-block-size: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
  }
  .terminal-bar {
    display: flex;
    /* Last, not first: a notice can wrap above the prompt's own line. */
    align-items: last baseline;
    gap: 1.5rem;
  }
  .terminal-output > * + *,
  .terminal-chat > * + * {
    margin-block-start: 0.2rem;
  }
  /* Chat mode: a prompt with data-chat swaps the log for #lobby. */
  .terminal-chat {
    display: none;
    flex: 0 1 auto;
    min-block-size: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
  }
  .terminal:has(#terminal-prompt[data-chat]) .terminal-chat {
    display: block;
  }
  .terminal:has(#terminal-prompt[data-chat]) .terminal-output {
    display: none;
  }
  .chat-line {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 1.5ch;
  }
  .chat-text {
    overflow-wrap: anywhere;
  }
  /* On a phone a name column squeezes the text; flow lines like IRC. */
  @media (max-width: 40rem) {
    .chat-line {
      display: block;
    }
    .chat-name {
      margin-inline-end: 1ch;
    }
  }
  .terminal-prompt:has(.terminal-notice) {
    flex-wrap: wrap;
  }
  .terminal-notice {
    flex-basis: 100%;
  }
  .terminal-leave {
    flex: none;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
  }
  /* A prompt is aria-busy while its request is in flight (data-indicator).
     The hint fades in only if that lasts 3 s, so quick replies never flash
     it; the delay is the transition's, so no timer is needed. */
  .terminal-prompt {
    position: relative;
    min-inline-size: 0;
  }
  /* It floats just above the prompt's end, so it never takes room. */
  .terminal-sending {
    position: absolute;
    inset-block-end: 100%;
    inset-inline-end: 0;
    opacity: 0;
    pointer-events: none;
  }
  .terminal-prompt[aria-busy="true"] .terminal-sending {
    opacity: 1;
    transition: opacity 200ms 3s;
  }
  .terminal-prompt {
    flex: 1;
    display: flex;
    align-items: baseline;
    gap: 1ch;
    padding-block-start: 0.35rem;
  }
  .terminal-prompt input {
    flex: 1;
    min-inline-size: 0;
    padding: 0;
    border: 0;
    outline: 0;
    background: transparent;
    color: inherit;
  }
  .terminal-echo .terminal-ps1 {
    margin-inline-end: 1ch;
  }
  .terminal-list {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 0.1rem 2ch;
    margin: 0.2rem 0 0.4rem;
    padding: 0;
    list-style: none;
  }
  .terminal-list dd {
    margin: 0;
  }
  .terminal-posts {
    grid-template-columns: 1fr;
  }

  /* Guestbook: a postcard to sign, then the notes pinned to a wall, newest
     first. The note and the address sit side by side where there's room,
     as on a real card, and stack on a phone. */
  /* The card is its own container: its two halves sit side by side only
     when the card itself is wide enough, wherever the page puts it. */
  .postcard {
    display: grid;
    gap: 1rem;
    margin-block-end: clamp(2.5rem, 7vw, 4rem);
    padding: clamp(1.1rem, 4vw, 1.75rem);
    container: postcard / inline-size;
  }
  .postcard-face {
    display: grid;
    gap: 1.25rem 2rem;
  }
  @container postcard (min-width: 30rem) {
    .postcard-face {
      grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
    }
    .postcard-address {
      padding-inline-start: 2rem;
    }
  }
  .postcard-note {
    display: grid;
    gap: 0.4rem;
  }
  .postcard textarea {
    inline-size: 100%;
    padding: 0;
    border: 0;
    resize: none;
  }
  .postcard-left {
    justify-self: end;
  }
  .postcard-address {
    display: grid;
    align-content: end;
    gap: 0.9rem;
  }
  .postcard-line {
    display: grid;
    grid-template-columns: 3.25rem minmax(0, 1fr);
    align-items: baseline;
  }
  .postcard-line input {
    inline-size: 100%;
    padding: 0.15rem 0;
    border: 0;
    border-radius: 0;
  }
  .postcard-stamp {
    justify-self: end;
    margin-block-start: 0.6rem;
    padding: 0.55rem 1rem;
    cursor: pointer;
  }
  .postcard-stamp:disabled {
    cursor: progress;
  }
  /* Once sent, the card is postmarked at its end. The mark is in the flow,
     so a one-line card grows to hold it. */
  .postcard-sent {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 1.5rem;
  }
  .postmark {
    grid-column: 2;
    grid-row: 1;
    display: grid;
    place-content: center;
    gap: 0.1rem;
    inline-size: 5.75rem;
    aspect-ratio: 1;
    border-radius: 50%;
    text-align: center;
  }

  .guestbook-section + .guestbook-section {
    margin-block-start: clamp(2.5rem, 7vw, 4rem);
  }
  .guestbook-heading {
    margin-block-end: 1rem;
  }
  /* Where main has room past the text column, the guestbook spreads out:
     the postcard and the doodle side by side (the two things to do here),
     then the wall of notes across both, so it holds more cards to a row. */
  @container main (min-width: 52rem) {
    main > .guestbook {
      grid-column: full;
      display: grid;
      /* The postcard's column grows first; the board takes the rest, and
         sizes itself up to 26rem within it. */
      grid-template-columns: minmax(0, var(--measure)) minmax(18rem, 1fr);
      align-items: start;
      column-gap: clamp(2.5rem, 5cqi, 5rem);
      max-inline-size: 76rem;
    }
    .guestbook > :is(.page-header, .postcard) {
      grid-column: 1;
    }
    /* The board may stand taller than the postcard, so it keeps the same
       space below it before the wall. */
    .guestbook-board {
      grid-column: 2;
      grid-row: 2;
      margin-block-end: clamp(2.5rem, 7vw, 4rem);
    }
    .guestbook-wall {
      grid-column: 1 / -1;
    }
    .guestbook-section + .guestbook-section {
      margin-block-start: 0;
    }
  }
  /* The shared board: as wide as the column allows, up to a comfortable
     square. */
  .guestbook-doodle {
    display: block;
    --sb-pixel-board-size: min(100%, 26rem);
  }
  /* Until the component loads (or without script), hold its square so the
     notes below don't jump when it arrives. */
  .guestbook-doodle:not(:defined) {
    inline-size: min(100%, 26rem);
    aspect-ratio: 1;
  }

  /* The wall: notes stack in columns, each as tall as it needs, like cards
     pinned close together. */
  .guestbook-entries {
    columns: 15rem;
    column-gap: 1.25rem;
    padding: 0;
    list-style: none;
  }
  .guestbook-entry {
    break-inside: avoid;
    margin-block-end: 1.5rem;
    padding: 1rem 1.15rem;
  }
  .guestbook-empty {
    column-span: all;
  }
  .guestbook-message {
    white-space: pre-line;
    overflow-wrap: anywhere;
  }
  .guestbook-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0 1ch;
    margin-block-start: 0.6rem;
  }

  /* Resume: sections of entries (a company, school, or project), each with
     its roles. Where main has room beside the text column, an entry's or
     role's dates hang in the margin like a sidenote; otherwise they sit
     under the heading. */
  .resume-section {
    margin-block-start: clamp(2.5rem, 6vw, 3.5rem);
  }
  .resume-header + .resume-section {
    margin-block-start: 0;
  }
  .resume :is(h2, h3) {
    position: relative;
    scroll-margin-block-start: 2rem;
  }
  .resume-section > h2 {
    margin-block-end: 1.5rem;
  }
  .resume-entry + .resume-entry {
    margin-block-start: 2.25rem;
  }
  .resume-entry-head,
  .resume-role-head {
    position: relative;
  }
  .resume-dates {
    margin-block-start: 0.2rem;
  }
  .resume-role {
    margin-block-start: 1.1rem;
  }
  :is(.resume-entry-head, .resume-role-head) + .resume-body {
    margin-block-start: 0.6rem;
  }
  .resume-body > * + * {
    margin-block-start: 0.7em;
  }
  .resume-body ul {
    padding-inline-start: 1.2em;
  }
  .resume-body li + li {
    margin-block-start: 0.35em;
  }
  @container main (min-width: 50rem) {
    .resume-dates {
      position: absolute;
      inset-block-start: 0.35em;
      inset-inline-start: calc(100% + var(--sidenote-gap));
      inline-size: 7rem;
      margin: 0;
    }
  }
  /* A list right under a section (Toolkit) is a list of labels: each
     item's bold lead hangs in a column of its own. It floats into the
     item's padding, so the space after it ("**Tooling** Turborepo…") starts
     the line and is dropped, and wrapped lines align with the first. */
  .resume-section > .resume-body ul {
    padding: 0;
    list-style: none;
  }
  .resume-section > .resume-body li {
    display: flow-root;
    padding-inline-start: 7.5rem;
  }
  .resume-section > .resume-body li > strong:first-child {
    float: inline-start;
    inline-size: 7.5rem;
    margin-inline-start: -7.5rem;
  }
  @container main (max-width: 30rem) {
    .resume-section > .resume-body li {
      padding: 0;
    }
    .resume-section > .resume-body li > strong:first-child {
      float: none;
      display: block;
      margin: 0;
    }
    .resume-section > .resume-body li + li {
      margin-block-start: 0.75em;
    }
  }
  /* A paragraph of nothing but code spans is a project's tags. */
  .resume-body p:has(> code):not(:has(> :not(code))) {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
  }
  /* A story: a quote collapsed behind its summary. */
  .resume-story {
    margin-block-start: 1rem;
  }
  .resume-story summary {
    inline-size: fit-content;
    list-style: none;
    cursor: pointer;
  }
  .resume-story summary::-webkit-details-marker {
    display: none;
  }
  .resume-story-body {
    margin-block-start: 0.6rem;
    padding-inline-start: 1rem;
  }
  .resume-story-body > * + * {
    margin-block-start: 0.7em;
  }
  .resume-intro > * + * {
    margin-block-start: 0.9rem;
  }
  .resume-actions {
    display: flex;
    flex-wrap: wrap;
    padding: 0;
    list-style: none;
  }
  .resume-actions li + li::before {
    content: "·";
    padding-inline: 1ch;
  }
  .resume-actions button {
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    cursor: pointer;
  }

  /* The terminal waits behind a trigger at the end of the breadcrumb row,
     and opens in a sheet pinned to the bottom. */
  /* A long breadcrumb wraps beside the trigger, which keeps to the first
     line. */
  .column-head {
    display: flex;
    align-items: baseline;
    gap: 1.5rem;
  }
  .column-head .breadcrumb {
    flex: 1;
    min-inline-size: 0;
  }
  .terminal-trigger {
    display: flex;
    align-items: baseline;
    flex: none;
    gap: 1.25ch;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    cursor: pointer;
  }
  /* Until terminal.js defines <wlls-terminal> (or without script at all),
     the trigger can't open anything, so it rests dimmed and inert. */
  body:has(wlls-terminal:not(:defined)) .terminal-trigger {
    opacity: 0.4;
    pointer-events: none;
  }
  /* The dialog has no padding, so a click on the dialog element itself
     can only be on its backdrop; the terminal inside carries the spacing. */
  .terminal-sheet {
    position: fixed;
    inset: auto 0 0;
    inline-size: 100%;
    max-inline-size: none;
    block-size: min(60dvh, 28rem);
    max-block-size: none;
    margin: 0;
    padding: 0;
    border: 0;
  }
  .terminal-sheet[open] {
    display: flex;
  }
  .terminal-sheet .terminal {
    flex: 1;
    padding-block: 1.25rem max(1.25rem, env(safe-area-inset-bottom));
    padding-inline: var(--pad-inline);
  }
  .terminal-hint {
    flex: none;
  }
  /* Where the rail drops below the content, the sheet takes the whole
     screen for every command, #lobby included, and the page behind holds
     still. With no backdrop left to tap, a close button sits at the top.
     The sheet covers the whole layout viewport; <wlls-terminal> pads it to
     the visual viewport, which shrinks for the on-screen keyboard where the
     layout viewport doesn't, and pins the page with terminal-pinned (iOS
     scrolls past overflow: hidden). A drag on the sheet pans nothing; only
     the log and #lobby scroll. */
  .terminal-close {
    display: none;
  }
  @media (max-width: 59.99rem) {
    .terminal-sheet {
      flex-direction: column;
      inset: 0;
      block-size: 100%;
      padding-block: var(--sheet-above, 0) var(--sheet-below, 0);
      touch-action: none;
    }
    .terminal-sheet :is(.terminal-output, .terminal-chat) {
      touch-action: pan-y;
    }
    html.terminal-pinned body {
      position: fixed;
      inset-inline: 0;
      inset-block-start: var(--pinned-top, 0);
    }
    .terminal-close {
      display: flex;
      justify-content: end;
      padding-block-start: max(0.5rem, env(safe-area-inset-top));
      padding-inline: var(--pad-inline);
    }
    /* A full-size tap target, its text still flush with the prompt's end. */
    .terminal-close button {
      min-block-size: 2.75rem;
      margin-inline-end: -0.75rem;
      padding: 0 0.75rem;
      border: 0;
      background: none;
      cursor: pointer;
    }
  }
  @media (hover: none) and (pointer: coarse) {
    .terminal-hint {
      display: none;
    }
  }
  @media (prefers-reduced-motion: no-preference) {
    .terminal-sheet,
    .terminal-sheet::backdrop {
      transition:
        translate 320ms var(--ease-out),
        opacity 320ms var(--ease-out),
        overlay 320ms allow-discrete,
        display 320ms allow-discrete;
    }
    .terminal-sheet:not([open]) {
      translate: 0 100%;
    }
    .terminal-sheet:not([open])::backdrop {
      opacity: 0;
    }
    @starting-style {
      .terminal-sheet[open] {
        translate: 0 100%;
      }
      .terminal-sheet[open]::backdrop {
        opacity: 0;
      }
    }
  }

  /* Entries are separated by space alone; the garden decides how a
     hovered entry announces itself. */
  .post-list {
    display: grid;
    gap: 0.25rem;
    padding: 0;
    list-style: none;
  }
  .post-entry {
    position: relative;
    display: grid;
    gap: 0.2rem;
    padding-block: 0.8rem;
  }
  .post-entry h2 {
    font-size: 1.1rem;
  }
  /* The title link covers the whole entry. */
  .post-entry h2 a::after {
    content: "";
    position: absolute;
    inset: 0;
  }

  .callout {
    padding: 1rem 1.25rem;
  }
  .callout > * + * {
    margin-block-start: 0.6em;
  }

  /* Footnote previews render in the top layer; script sets left/top. */
  .footnote-preview {
    position: fixed;
    inset: auto;
    margin: 0;
    max-inline-size: min(24rem, 100vw - 1rem);
    padding: 0.8rem 1rem;
  }
  .footnote-preview > * + * {
    margin-block-start: 0.5em;
  }

  .message-page {
    min-block-size: 45vh;
    display: grid;
    align-content: center;
    justify-items: start;
    gap: 1rem;
  }

  /* Print: the page alone, without the shell around it. Footnotes print as
     the notes section, since a sheet has no margin to hang them in. */
  @media print {
    .skip-link,
    .column-head,
    .rail,
    .terminal-sheet,
    .footnote-preview,
    .heading-anchor,
    .sidenote {
      display: none;
    }
    .footnotes {
      display: block;
    }
    .shell {
      display: block;
      min-block-size: 0;
    }
    .column {
      padding: 0;
    }
    main {
      grid-template-columns: [full-start text-start wide-start] minmax(0, 1fr) [text-end wide-end full-end];
      padding-block-start: 0;
    }

    /* The resume prints on its own page size (the @page rule below),
       without its stories or links to its other forms. Dates
       move to the end of their heading's line. */
    .resume {
      page: resume;
    }
    .resume-actions,
    .resume-story,
    .resume-header .article-date {
      display: none;
    }
    .resume-header {
      margin-block-end: 1.1rem;
    }
    .resume-header > * + * {
      margin-block-start: 0.3rem;
    }
    .resume-section {
      margin-block-start: 1.1rem;
    }
    .resume-section > h2 {
      margin-block-end: 0.6rem;
    }
    .resume-entry + .resume-entry {
      margin-block-start: 0.9rem;
    }
    .resume-role {
      margin-block-start: 0.45rem;
    }
    :is(.resume-entry-head, .resume-role-head) + .resume-body {
      margin-block-start: 0.25rem;
    }
    .resume-body li + li {
      margin-block-start: 0.15em;
    }
    .resume-entry-head,
    .resume-role-head {
      display: flex;
      justify-content: space-between;
      align-items: baseline;
      gap: 1.5rem;
    }
    .resume-dates {
      position: static;
      flex: none;
      inline-size: auto;
      margin: 0;
    }
    /* Keep headings with what follows them, and short things whole: a
       bullet, an entry without roles (a project, a school), the toolkit.
       Longer roles may break between bullets, so a page never ends in a
       gap the next role couldn't fill. */
    .resume :is(h2, h3, h4),
    .resume-entry-head,
    .resume-role-head {
      break-after: avoid;
    }
    .resume-body li,
    .resume-entry:not(:has(.resume-role)),
    .resume-section > .resume-body {
      break-inside: avoid;
    }
  }
}

/* Page boxes can't be layered. Every printed page gets a plain margin; the
   resume's named page is US letter and a little tighter, to fit two sheets. */
@page {
  margin: 0.75in;
}
@page resume {
  size: letter;
  margin: 0.5in 0.6in;
}
