html { scroll-behavior: smooth; }
  body { margin: 0; background: #0C0C0C; color: #D6D2CB; font-family: "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace; font-size: 15px; line-height: 1.7; }
  a { color: #6FA8FF; }
  a:hover { color: #E0A33C; }
  @keyframes caret { 0%, 45% { opacity: 1 } 46%, 100% { opacity: 0 } }

  /* The block caret at the end of each prompt. Its parent is a flex row, so the
     vertical-align it used to carry did nothing at all — vertical-align has no
     effect on a flex item — and the block was left at the cross-start of the line
     with its bottom edge exactly on the text baseline. Measured at 1280x900, that
     put its centre 2.89px above the centre of the capitals beside it in all eight
     prompts, which reads as a caret sitting slightly high.

     Baseline on the row is what anchors it: it makes the caret's bottom edge the
     text baseline by construction, rather than by the coincidence that the block
     is exactly as tall as the line's ascent at this line-height. Two things that
     do not work, both tried and measured: align-self on the caret alone, because
     the only baseline-aligned item in a line has nothing to share a baseline with
     and falls back to cross-start; and a negative bottom margin, because a flex
     item with no text has its baseline synthesized from the border box, so the
     margin changes the line's size and not the block's position. The 3px that
     centres it is therefore painted, not laid out — relative offset, no effect on
     anything around it. */
  [data-prompt] { align-items: baseline }
  [data-caret] { position: relative; top: 3px }

  /* ~/shell is a terminal, and a terminal has a window: the box is a fixed height
     with its own scrollbar, and scrollShell() in app.js pins it to the bottom after
     every command. Before it, `help` — twenty lines — grew the section, pushed the
     rest of the page down and left the new prompt below the fold. The region needs
     no tabindex of its own: the prompt input lives inside it.

     Only the scrollbar is styled here. The height and the overflow are inline in
     index.html, and that is deliberate: this file is served max-age=300 while the
     HTML is not cached at all, so for five minutes after a deploy a visitor can be
     running new markup against the previous stylesheet. That window is harmless for
     a colour and not for a geometry — it shipped once, and because the markup had
     already dropped its old min-height the box came out 64px tall and growing,
     which is worse than either version. Structure that the markup depends on goes
     in the markup. */
  [data-term] {
    scrollbar-width: thin;
    scrollbar-color: #7FC8A9 #0E0E0D;
  }
  [data-term]::-webkit-scrollbar { width: 8px }
  [data-term]::-webkit-scrollbar-track { background: #0E0E0D }
  [data-term]::-webkit-scrollbar-thumb { background: #2A2724 }
  [data-term]::-webkit-scrollbar-thumb:hover { background: #7FC8A9 }
  @keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
  @keyframes introOut { to { clip-path: inset(0 0 100% 0); opacity: 0; visibility: hidden } }
  @keyframes decode { from { clip-path: inset(0 0 100% 0) } to { clip-path: inset(0 0 0 0) } }
  @keyframes bar { from { width: 0 } to { width: 100% } }
  /* The boot log scrolls only as far as it overflows; app.js measures that and
     sets --boot-shift. Untouched, this animation moves nothing. */
  @keyframes bootScroll { to { transform: translateY(var(--boot-shift, 0px)) } }
  @keyframes wmType { to { clip-path: inset(0 0 0 0) } }
  @keyframes wmCaretOn { to { opacity: 1 } }
  @keyframes wmCaretOff { to { opacity: 0 } }
  /* The wordmark hands over to the page by dissolving, not by the hard clip the
     boot log uses — the log is a machine wiping its own output, this is not. */
  @keyframes stingOut { to { opacity: 0; visibility: hidden } }
  @keyframes wmOut { to { opacity: 0; transform: translateY(-10px) scale(1.03); filter: blur(4px) } }
  ::selection { background: #7FC8A9; color: #0C0C0C }
  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { animation-duration: .01ms !important; animation-delay: 0s !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important }
    /* The nav and skip links all jump within the page, and a smooth scroll is
       motion the visitor has asked the OS not to show them. */
    html { scroll-behavior: auto !important }
  }

  /* Visually hidden, still read aloud. The page is a terminal simulation, so
     its structure has to be carried by headings a screen reader can navigate
     rather than by anything on screen. */
  .sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0 }

  /* The two ls listings are grids of max-content columns, which cannot shrink.
     On a phone the decorative permission and owner columns are dropped so the
     filename gets the width instead of the page scrolling sideways. */
  @media (max-width: 560px) {
    .ls { grid-template-columns: max-content minmax(0, 1fr) !important }
    .lsx { grid-template-columns: minmax(0, 1fr) !important }
    .ls-dim { display: none }

    /* Even with the mode and owner columns gone, the issue date is `max-content`
       and takes 88px of 292, so six of the ten names wrap — `github-advanced-` /
       `security.cert`. At line-height 1.9 and no row-gap the second line of one
       entry sits exactly as far from the first as the next entry does, so a
       wrapped name reads as two certificates. 1.5 pulls the wrap together and
       8px of row-gap pushes the entries apart, which is the distinction the
       reader actually needs; the gutter gives back a character on the way. */
    .ls { line-height: 1.5 !important; row-gap: 8px; column-gap: 1ch !important }

    /* 14px of side padding leaves the ~/certs name column 200px, and the longest
       chunk a browser will not break — `foundations.cert.expired`, no hyphen to
       break at and no rule that breaks at a dot — measures 201.6px. Two pixels,
       and `overflow-wrap: anywhere` pays for them by cutting the word in half:
       `oracle-oci-foundations.cert.expire` / `d`. 12px hands back four, which
       every section below 560px gets to spend on its own measure. The safe-area
       inset stays: on a notched phone in landscape this is what keeps text out
       from under the corner. */
    main { padding-left: calc(12px + env(safe-area-inset-left, 0px)) !important; padding-right: calc(12px + env(safe-area-inset-right, 0px)) !important }
  }

  /* Long URLs are unbreakable strings. `anywhere` (not `break-word`) is what
     also shrinks the grid column's min-content size, so ~/contact stops
     widening the page below ~400px. */
  main a { overflow-wrap: anywhere }

  /* The nav is a 34px fixed bar and the status line a 30px fixed footer, and
     the browser's scroll-into-view on Tab knows about neither. Without this a
     focused link or the shell input can come to rest completely underneath one
     of them (WCAG 2.2 SC 2.4.11). scroll-margin-top on the sections only helps
     the anchors, not what is inside them.

     Since viewport-fit=cover, both bars are as tall as themselves plus the inset
     they sit in, so everything that has to clear them carries the inset too. The
     section rule needs !important: the value it overrides is an inline style. On
     anything without a notch every env() here is 0px and none of this moves. */
  main a, main button, main input { scroll-margin-block: calc(48px + env(safe-area-inset-top, 0px)) calc(40px + env(safe-area-inset-bottom, 0px)) }
  main section[id] { scroll-margin-top: calc(48px + env(safe-area-inset-top, 0px)) !important }
  .skip:focus { top: calc(42px + env(safe-area-inset-top, 0px)) !important }

  /* Without this, any `width:100%` that also has padding or a border is wider
     than its parent. That is what pushed ~/visitor's map box past the viewport
     on a phone. */
  *, *::before, *::after { box-sizing: border-box }

  /* ~/whoami is the only section carrying tabindex="-1", because it is where the
     skip link lands and without it Tab would carry on from the nav — the classic
     way a skip link fails. The side effect is that any jump to #whoami focuses the
     section, and Safari paints a ring around the whole thing: tapping 0:whoami in
     the tmux bar drew a box no other section draws, which Chrome never shows.
     :focus:not(:focus-visible) was tried and shipped and does not fix it — Safari
     counts the focus a fragment jump moves onto a tabindex="-1" element as
     focus-visible even when the jump came from a finger, so the guard never
     matches and the ring stays. Unconditional, then. What it costs is the box a
     keyboard user saw after the skip link, and that is the normal way skip links
     are built: the page still scrolls to the section, focus still sits there so
     the next Tab continues from inside it, and the first link in the section has
     its own visible focus style. Do not put this back behind :focus-visible, and
     do not take the tabindex off to avoid the ring — that is what breaks the skip
     link. */
  #whoami:focus { outline: none }

  /* ~/certs row colours. These live here rather than in a style attribute so
     the template never interpolates into CSS, which nothing can parse. */
  .cert-ok { color: #C5C0B8 }
  .cert-old { color: #8A857D }

  /* Each name links to its own badge page. `color: inherit` is the point: these
     rows carry validity in their colour, and that distinction is the reason the
     listing is generated at all, so a name must not turn link-blue on becoming a
     link. A dotted underline carries the affordance instead, and hover and focus
     go amber like every other link here. inline-block is what makes padding-block
     count toward the 24px WCAG 2.5.8 asks for — the rows are 21px of line at
     phone widths — and the box still wraps its own text, which six of the ten
     names need. */
  /* The negative margin cancels what the padding costs in layout and keeps what
     it buys in hit area. Without it the padding pushed each linked name 2px
     below the date beside it — measured, exactly 2.0px on all eight links and
     0.0 on the two plain rows — because the date is a separate grid cell and
     does not move with it. getBoundingClientRect still reports the border box,
     so the target stays 25px and measure.mjs still sees it. */
  .cert-link { color: inherit; display: inline-block; padding-block: 2px; margin-block: -2px; text-decoration: underline; text-decoration-style: dotted; text-decoration-color: #4E4E4A; text-underline-offset: 2px }
  .cert-link:hover { color: #E0A33C; text-decoration-color: #E0A33C }
  .cert-link:focus-visible { color: #E0A33C; text-decoration-color: #E0A33C; outline: 2px solid #7FC8A9; outline-offset: 2px }

  /* Below 936px the tmux bar cannot show all eight windows at once. It stays a
     scroller — the fix is that the scrollbar is now visible, so the swipe is
     discoverable instead of being a secret. The bar grows by the 6px the
     scrollbar occupies, and the skip link clears the taller bar. */
  @media (max-width: 936px) {
    nav { height: calc(42px + env(safe-area-inset-top, 0px)) !important; scrollbar-width: thin !important; scrollbar-color: #7FC8A9 #151513 }
    nav a, nav > span { line-height: 34px !important }
    nav::-webkit-scrollbar { height: 6px }
    nav::-webkit-scrollbar-track { background: #151513 }
    nav::-webkit-scrollbar-thumb { background: #7FC8A9 }
    .skip:focus { top: calc(50px + env(safe-area-inset-top, 0px)) !important }
    .navmore { display: flex !important }
  }

  /* Two inline links sat at 18-19px high, under the 24px WCAG 2.5.8 asks for.
     Padding, not a bigger font, so the neofetch grid keeps its rhythm.

     The negative margin is what keeps that promise. Padding on an inline-block
     pushes its first line down, and `Contact`'s label sits in a separate grid
     cell that does not move with it: measured at 1440, that row was the only one
     of the ten off its label, by exactly the 3px of padding. The margin cancels
     the layout cost and keeps the hit area — getBoundingClientRect reports the
     border box, so both links still measure ~32px. */
  a.tap { display: inline-block; padding-block: 3px; margin-block: -3px }

  /* A wrapped prompt used to start its second line one character in. The 1ch
     that separates `$` from the command is a margin-left on the command, so when
     the row wraps between them — `~/work$ head sys-0*.md` is 42 characters and
     339px of room holds 38 — the indent goes with it and the line hangs. Moving
     the same 1ch to the right edge of `$` puts it at the end of the line above,
     where it costs nothing, and the command starts at column 0 the way a
     terminal would. `$` is third from the end in all nine prompts: `$`, the
     command, the caret. */
  @media (max-width: 720px) {
    [data-prompt] > [data-cmd] { margin-left: 0 !important }
    [data-prompt] > span:nth-last-child(3) { margin-right: 1ch }
    /* ~/shell's history prints the same prompt, minus the caret, so `$` is second
       from the end there rather than third. */
    [data-term] [role="log"] > div > div:first-child > span:last-child { margin-left: 0 !important }
    [data-term] [role="log"] > div > div:first-child > span:nth-last-child(2) { margin-right: 1ch }

    /* `guest@mozolewski.eu:~$` is 185px of a 296px line and the input asks for
       140, so on a phone the input wrapped onto a line of its own — where, having
       no border and no background by decision, it was invisible. The page ended
       with a prompt and an empty box and no way to tell you could type in it. 56px
       lets it stay on the prompt line; flex:1 then hands it the 81px that are
       actually there, which is nine characters and scrolls like a narrow terminal.
       Do not answer this with a border: see the note on the focus indicator. */
    [data-term] input { min-width: 56px !important }
  }

  /* ~/work is the only long-form prose on the page and the narrowest place to
     read it: 15px monospace in 339px is 38 characters a line, half the measure
     the type wants, and at line-height 1.7 that made each body a 16-line, 408px
     block. 1.55 is the leading a 38-character measure asks for, and the 18px
     indent gives back 8px — a character — without letting the rule go.
     The `#` heading keeps its own 1.45; it is an inline style and wins. */
  @media (max-width: 720px) {
    .case { padding-left: 10px !important }
    .case > div { line-height: 1.55 }
  }

  /* ~/whoami's neofetch row wraps below ~734px — 206px of portrait and the data
     block's 420px minimum stop fitting side by side. Stacked, the portrait is
     235px tall on a 320px screen: measured, `marcin@mozolewski` began at y=1030
     and the whole first screen of the site was a photograph. Below 720px the row
     becomes a grid, the portrait sits beside the name at 26vw, and the
     Role/Host/… table spans the full width under both.

     Three things here are not optional. `gap` needs !important because .nf-row
     carries an inline `gap:clamp(20px,4vw,48px)` and an inline shorthand beats a
     stylesheet longhand — every row sat 20px apart until it did. The img needs a
     width because it carries width="192" and no CSS width, so it kept its
     intrinsic size and painted straight across the name. And the name's clamp
     floor drops to 17px here only: `marcin@mozolewski` is 17 characters of a
     0.6em-advance monospace and cannot wrap, so at 320px it needs 194px at 19px
     against the 195px the column has, which is not a margin.

     The transfer bar and its scp line follow the text, not the photo: under the
     photo they float 43px below it, and at 26vw the line wraps to three. */
  @media (max-width: 720px) {
    .nf-row { display: grid !important; grid-template-columns: clamp(76px, 26vw, 112px) minmax(0, 1fr); gap: 0 14px !important; align-items: start }
    .nf-logo, .nf-data { display: contents !important }
    .nf-shot { grid-column: 1; grid-row: 1 / span 4 }
    .nf-shot img { width: 100%; height: auto }
    .nf-name { grid-column: 2; grid-row: 1; font-size: clamp(17px, 5.5vw, 24px) !important }
    .nf-tag { grid-column: 2; grid-row: 2 }
    .nf-bar { grid-column: 2; grid-row: 3 }
    .nf-cap { grid-column: 2; grid-row: 4; margin-top: 6px }
    .nf-grid { grid-column: 1 / -1; grid-row: 5; margin-top: 16px }
  }

  /* ~/career's date column is max-content and cannot wrap, so on a phone it was
     taking two thirds of the row and leaving one word per line for the job
     title. Below 560px the same dates print short. */
  .d-short { display: none }
  @media (max-width: 560px) {
    /* the two 16px gutters are worth 32px of job title on a 320px screen */
    .cgrid { column-gap: 8px !important }

    /* The date column is `max-content` and cannot wrap, so beside the entry it
       took 92px of the 292px a 320px screen has — a third of the row — and left
       the text ~20 characters a line: the job title broke over two lines, the
       `(HEAD -> main)` tag fell onto a third, and the one-line summary ran to
       seven. Below 560px the date goes above the entry instead, which is where
       `git log` prints a date anyway, and the text gets 275px — 32 characters.

       Each entry stops being three cells of one big grid and becomes its own
       two-column grid, so the graph column can span both of its rows. .cgrid
       carries the 20px that used to be its row-gap, because the rule drawn down
       the graph column reaches 20px past the bottom of each entry to bridge it.

       Short dates were the workaround for the column this replaces, so the full
       ones come back: `2023-05 — 2024-09` is 143px of the 275 now available. */
    .cgrid { display: flex !important; flex-direction: column; gap: 20px }
    .cgrid > div { display: grid !important; grid-template-columns: 1ch minmax(0, 1fr); column-gap: 8px; align-items: start }
    .cgrid > div > :nth-child(1) { grid-column: 1; grid-row: 1 / span 2 }
    .cgrid > div > :nth-child(2) { grid-column: 2; grid-row: 1 }
    .cgrid > div > :nth-child(3) { grid-column: 2; grid-row: 2; margin-top: 2px }
  }

  /* The two boxes that stand where the map will be are 214px and 220px tall, so
     ~/visitor shows every visitor a third of a 320px screen of empty dashed
     rectangle — and most never press the button that fills it. They hug their
     text on a phone instead. Nothing is lost: what the box says, that the tiles
     come from openstreetmap.org, the note above the button already says before
     the press, which is where the disclosure has to be anyway. The press then
     grows the section rather than swapping equal heights, and that is the right
     trade — the button is above the box and does not move, so nothing shifts
     under the finger that just tapped it. */
  @media (max-width: 560px) {
    .map-ph { height: auto !important; padding: 20px 16px !important }
  }

  /* ~/contact's label column is 14ch, because `stackoverflow` is 13 characters
     wide. That leaves the value 154px of a 320px screen — 18 characters — so
     every one of the five URLs wrapped, and `overflow-wrap: anywhere` broke them
     where the room ran out rather than at a separator: `github.com/marcjane` /
     `k`, `contact@mozolewski.` / `eu`. The labels, centred against two-line
     values, floated free of them.

     Below 560px the label goes above its value and the value gets the whole
     296px. The label keeps its 9px of top padding and loses the bottom one, so
     the pair sits 9px apart and reads as a pair; the anchors keep theirs whole,
     which is what holds them over the 24px WCAG 2.5.8 asks for. */
  @media (max-width: 560px) {
    .crow { grid-template-columns: minmax(0, 1fr) !important; column-gap: 0 !important; align-items: start !important }
    .crow > :first-child { padding-bottom: 0 !important }
    /* 9px above the label and 9px above the value put 33px inside a pair against
       41px between pairs, which is not enough difference to group them. 2px
       above the value makes it 26 against 41. The anchors keep 9px below, so
       they still measure 34.8px against the 24px WCAG 2.5.8 asks for. */
    .crow > :nth-child(2) { padding-top: 2px !important }
  }

  /* ~/stack used to be drawn with U+2500 box characters. They are not in the
     JetBrains Mono subset, so they came from the OS fallback at a different
     advance width — on Windows that is Consolas, about 8% narrower, which bends
     the columns out of line. Same shape, drawn with borders that cannot drift. */
  .tree, .tree ul { list-style: none; margin: 0; padding: 0 }
  .tree li { position: relative; padding-left: 4ch }
  /* Where the elbow meets the stem is half a line down, and there is no unit for
     that: `em` is the font size, not the line box. --tree-half is that half,
     written out — .95em against line-height 1.9 — so the two move together. Change
     the line-height of .tree-box without changing this and every elbow slides off
     its label. */
  .tree { --tree-half: .95em }
  .tree li::before { content: ""; position: absolute; left: .5ch; top: 0; bottom: 0; border-left: 1px solid #C5C0B8 }
  .tree li:last-child::before { bottom: auto; height: var(--tree-half) }
  .tree li::after { content: ""; position: absolute; left: .5ch; top: var(--tree-half); width: 2.4ch; border-top: 1px solid #C5C0B8 }

  /* ~/stack is 26 rows of one or two words each, and 1.9 gives every one of them
     26.6px on a screen 320px wide — 931px of section for twenty labels. 1.6 is
     still loose for a list and gives back four pixels a row. */
  @media (max-width: 560px) {
    .tree-box { line-height: 1.6 !important }
    .tree { --tree-half: .8em }
  }

  /* The last character the subset did not carry: ~/shell's status dot, a lone
     U+25CF. It aligned nothing, so it was only ever a hairline of wrongness —
     but nothing on the page needs the OS fallback any more. */
  .dot { display: inline-block; width: .55em; height: .55em; border-radius: 50%; background: #7FC8A9; vertical-align: .06em }

  /* The two gauges were U+2588 and U+2591 for the same reason, and drifted the
     same way. Ten cells, one per tenth. */
  .gauge { display: inline-flex; vertical-align: -.15em }
  .gauge span { display: block; width: .6em; height: 1.05em }
  .g-on { background: #E0A33C }
  .g-off { background: #4E4E4A }

  /* The nine section headings were .sr, so the document had an outline for a
     screen reader and none for an eye: the only thing marking a boundary was
     the 12px prompt bar, identical in all nine. They carry the human name
     while the bar keeps the path, so nothing is said twice.
     The UA defaults have to go with them — sections lay their children out
     with flex `gap`, and h2's own `margin: .83em 0` would break the rhythm of
     all nine at once. `main h2` cannot match the h1, which stays .sr. */
  main h2 { margin: 0; font-size: clamp(20px, 2.2vw, 23px); font-weight: 500; line-height: 1.3; color: #F3EFE9; letter-spacing: -.01em }
